Improved submodule support by rayvincent2 · Pull Request #670 · node-config/node-config

Goals

Implications (Backwards Compatibility)

  • setModuleDefaults will now throw an error if it's called more than once for the same moduleName.
    • To ensure consistency for each config instance used in the module, we must enforce that the module only register it's defaults once. Likely in the module's main file. I don't expect this to be an issue, but would need your feedback. I don't see how calling this method more than once up to now would give predictable behavior.
  • getModuleConfig(moduleName: string, options: object | undefined) method was added to return a new Config instance which merges the provided options into the module's registered default configs and returns a new Config instance. This method will throw an error if the setModuleDefaults was never called for the given module.
  • The Wiki page will need some updates to the submodule page to showcase a few approaches.
  • Everything else is 100% backwards compatible. See the added unit tests for new features. All unit tests are passing.

Solved Issues

I started this branch to solve the Late Loading portion of #572, but am of the opinion that all acceptance criteria of #572 are met and the ticket can be closed. I saw an opportunity to improve module configuration support which closes #226 as well. Let me know if you'd rather me split this into two PRs.

Issue #572, Late Loading

I found that the setModuleDefaults method would merge the module defaults into the root config object. This is obviously a problem when my module is dynamically loaded after config.get was called and the root config object was immutable and wouldn't accept my module defaults.

  1. I decided to create a separate moduleConfigs object to store module default configs. The module defaults will get any existing overrides in the global config merged in. Once set on the global moduleConfigs object, only that modules entry will be made immutable immediately.
    • By only setting that current module entry as immutable, it allows for additional modules to be added at any time.
  2. The config.get method was updated to look in the moduleConfigs object for keys if they come back undefined on the global config object.

Issue #226, Allow multiple instances of modules

Please raise issue with any misunderstanding that I have about the current problems regarding multiple instances of modules. I read through issues #226 and I believe that I've found that adding one new method getModuleConfig provides a clean way to create new Config instances which can share the lifecycle of the module instances.

Issue #572, Submodule config as default

With the loadFileConfigs method, It appears that this is already supported. As a user of this library, I've never found it difficult to simply add the following lines to my submodule's entry point:

var config = require('config');
var path = require('path');
const baseConfig = config.util.loadFileConfigs(path.join(__dirname, '..', 'config'));

This is hardly a burden and the APIs clearly supports this already.

Issue #572, Dual purpose

Again, if developers write their modules to load configs from the expected file locations, I don't see why this isn't already supported. If the ask is to allow modules to access config keys without specifying the root module name, then the approach I provided in the examples may help. Again, I don't see how this isn't already supported.

Side effects

Accessing module default configs

Since the default module settings exist separate from the global config object, then you can access the root module configs from any Config instance. I see this as a unexpected benefit. This means that the getModuleConfig instances created will allow you to access the module's default configurations.

var config = require('config');
config.util.setModuleDefaults('TestModule', { param1: 'value1' });
var moduleConfig = config.util.getModule('TestModule', { param1: "Different Value" });

console.log(moduleConfig.get('param1'));
// Outputs overridden module config "Different Value"

console.log(moduleConfig.get('TestModule.param1'));
// Outputs module default config "value1"

Examples

Module returns one class

Here's an example module that would provide this behavior. I added something similar to the unit tests in this PR.

var config = require('config');

// Set the module defaults
config.util.setModuleDefaults('TestModule', {
  test: true,
  example: true
});

function TestModule(options) {
  // Get a unique module config based on your defaults with optional override
  this.moduleConfig = config.util.getModuleConfig('TestModule', options);
}

TestModule.prototype.isExample = function() {
  return this.moduleConfig.get('example');
};

module.exports = TestModule;

This allows the application to then use the module multiple times. Each instance of the TestModule class being able to have unique configuration and override as needed. The trick simply being that each instance would need to manage the retrieved configuration from getModuleConfig.

Module returns multiple classes with nested config scopes per class

var config = require('config');

// Set the module defaults
config.util.setModuleDefaults('TestModule', {
  server: {
    host: 'localhost',
    port: 8080
  },
  client: {
    host: 'localhost',
    port: 8080,
    requestTimeout: 10000
  },
  logging: true
});

function Server(options) {
  // Request module configs, but only override server settings
  var moduleConfig = config.util.getModuleConfig('TestModule', {  server: options || {} });
  this.config = moduleConfig.get('server');

  if (this.config.get('TestModule.logging')) {
    console.log('Server Created');
  }
}

function Client(options) {
  // Request module configs, but only override client settings
  var moduleConfig = config.util.getModuleConfig('TestModule', {  client: options || {} });
  this.config = moduleConfig.get('client');

  if (this.config.get('TestModule.logging')) {
    console.log('Client Created');
  }
}

module.exports = {
  Server: Server,
  Client: Client
};