Improved submodule support by rayvincent2 · Pull Request #670 · node-config/node-config
Goals
- Ensure modules that leverage node-config could be loaded dynamically (well after
config.getwas called) and the applications configuration became immutable (closes [feature] Improved submodule support #572 - Late Loading) - Ensure that a submodule could have multiple instantiated classes which could leverage the modules default/overridden configs (closes Find a solution to the multiple-instance problem for submodule defaults #226)
Implications (Backwards Compatibility)
setModuleDefaultswill now throw an error if it's called more than once for the samemoduleName.- 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 newConfiginstance 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 thesetModuleDefaultswas 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.
- I decided to create a separate
moduleConfigsobject to store module default configs. The module defaults will get any existing overrides in the global config merged in. Once set on the globalmoduleConfigsobject, 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.
- The
config.getmethod was updated to look in themoduleConfigsobject 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 };