Config provider
Modular configuration provider. Reads and validates configuration from:
- Default option values
- Configuration received programmatically
- Configuration Files. Using Cosmiconfig
- Environment variables
- Command line arguments
As a summary, it also provides:
- Automatic config validation
- Isolated configuration namespaces
- Objects to get/set options
- Events when any value changes
Different namespaces can be created for each different element in the architecture. So, each different component is the unique who knows about its own options, but the user can define options for all components at a time, and using the same methods.
This module provides configuration to Mocks Server components and plugins, but it may be used anywhere else because it is fully configurable.
Usage
A brief example:
const Config = require("@mocks-server/config");
const config = new Config({ moduleName: "mocks" });
const namespace = config.addNamespace("fooNamespace");
const option = namespace.addOption({
name: "fooOption",
type: "string",
default: "foo-value",
});
config.load().then(() => {
console.log(option.value);
option.value = "foo-new-value";
});
option.onChange((newValue) => {
console.log(`Option has a new value: ${newValue}`);
});
Configuration sources
Once options and namespaces are added, and the load
method is called, the library searches for values for the options in next sources, sorted from lower to higher priority:
- Default option value. When the option is created, it can contain a
default
property. That value will be assigned if no other one is found. - Programmatic config. An object containing initial configuration can be provided to the
load
or init
methods. - Configuration files. Configuration files in
process.cwd()
. Cosmiconfig is used to provide this feature, so it is compatible with next files formats:
- A
[moduleName]
property in a package.json
file - A
.[moduleName]rc file with JSON or YAML syntax.
- A
.[moduleName]rc.json
, .[moduleName]rc.yaml
, .[moduleName].yml
, .[moduleName]rc.js
, or .[moduleName]rc.cjs
file. - A
[moduleName].config.js
or [moduleName].config.cjs
CommonJS module exporting the object. - A
[moduleName].config.js
or [moduleName].config.cjs
CommonJS module exporting a function. It receives programmatic configuration as first argument. - A
[moduleName].config.js
or [moduleName].config.cjs
CommonJS module exporting an async function. It receives programmatic configuration as first argument.
- Environment variables. Environment variables can be also used to define options values.
- Process arguments. Commander is used under the hood to get values from command line arguments and assign them to each correspondent option.
Options
Options can be added to a namespace, or to the root config object. Both config
and namespaces have an addOption
method for creating them. Check the API chapter for further info.
Options can be of one of next types: string
, boolean
, number
, object
or array
. This library automatically converts the values from command line arguments and environment variables to the expected type when possible. If the conversion is not possible or the validation fails an error is thrown. Validation errors provide enough context to users to let them know the option that failed. This library uses ajv
and better-ajv-errors
for validations.
Here is an example of how to add an option to the root config object, and then you have information about how the option would be set from different sources:
const config = new Config({ moduleName: "mocks" });
config.addOption({ name: "optionA", type: "string", default: "foo" });
- Configuration from files or programmatic: The option must be defined at first level:
{
optionA: "foo-value"
}
- Configuration from environment variables: The
moduleName
option must be prepended to the option name, using "screaming snake case".
MODULE_NAME_OPTION_A="foo-value"
- Configuration from arguments: The argument must start by "--".
--optionA="foo-value"
Namespaces
Different isolated namespaces can be created to provide configuration to different components in the architecture. For example, Mocks Server uses config namespaces in order to allow plugins to define their own options without having conflicts with core options or other plugins options.
const config = new Config({ moduleName: "moduleName" });
config
.addNamespace("namespaceA")
.addOption({ name: "optionA", type: "string", default: "foo" });
When a namespace is created, its name must also be used when providing the configuration to its options. Next patterns are used to define the namespace of an option:
- Configuration from files or programmatic: The option must be defined inside an object which key corresponds to the namespace name:
{
namespaceA: {
optionA: "foo-value"
}
}
- Configuration from environment variables: The
moduleName
option and the namespace name must be prepended to the option name, using "screaming snake case".
MODULE_NAME_NAMESPACE_A_OPTION_A="foo-value"
- Configuration from arguments: The namespace name must be prepended to the option name, and separated by a dot. The argument must start by "--".
--namespaceA.optionA="foo-value"
Nested namespaces
Namespaces can be nested, so they can contain also another namespaces apart of options. The previous rules about how to use namespaces when giving values to options are also valid for nested namespaces. For example:
const config = new Config({ moduleName: "mocks" });
config
.addNamespace("namespaceA")
.addNamespace("namespaceB")
.addOption({ name: "optionA", type: "string", default: "foo" });
- Configuration from files or programmatic: Namespace names must be nested properly.
{
namespaceA: {
namespaceB: {
optionA: "foo-value"
}
}
}
- Configuration from environment variables: All parent namespace names must be prepended:
MOCKS_NAMESPACE_A_NAMESPACE_B_OPTION_A="foo-value"
- Configuration from arguments: All parent namespace names must be prepended and separated by a dot:
--namespaceA.namespaceB.optionA="foo-value"
Option types
String
Options of type string
are not parsed in any way.
const config = new Config({ moduleName: "moduleName" });
const option = config.addOption({
name: "optionA",
type: "string",
});
await config.load();
Examples about how to define options of type string
from sources:
- Programmatic or configuration files:
{"optionA": "foo-value"}
- Environment variables:
MODULE_NAME_OPTION_A=foo-value
- Arguments:
node myProgram.js --optionA=foo-value
Boolean
Options of type boolean
are parsed when they are defined in environment variables, and they have a special format when defined in arguments:
const option = config.addOption({
name: "optionA",
type: "boolean",
default: true,
});
await config.load();
Examples about how to define options of type string
from sources:
- Programmatic or configuration files:
{"optionA": false}
- Environment variables:
0
and false
strings will be parsed into boolean false
: MODULE_NAME_OPTION_A=false
, or MODULE_NAME_OPTION_A=0
.- Any other value will be considered
true
: MODULE_NAME_OPTION_A=true
, MODULE_NAME_OPTION_A=1
or MODULE_NAME_OPTION_A=foo
.
- Arguments:
- If the option has default value
true
, then the --no-
prefix added to the option name will produce the option to have a false
value: node myProgram.js --no-optionA
- If the option has a
false
default value, then the option name will be set it as true
: node myProgram.js --optionA
Number
Options of type number
are parsed when they are defined in environment variables or command line arguments.
const option = config.addOption({
name: "optionA",
type: "number"
});
await config.load();
Examples about how to define options of type number
from sources:
- Programmatic or configuration files:
{"optionA": 5}
- Environment variables:
MODULE_NAME_OPTION_A=5
, or MODULE_NAME_OPTION_A=5.65
. - Arguments:
node myProgram.js --optionA=6.78
Object
Options of type object
are parsed from JSON strings when they are defined in environment variables or command line arguments. It is important to mention that objects are merged when they are defined in different sources when loading the configuration.
const option = config.addOption({
name: "optionA",
type: "object"
});
await config.load();
Examples about how to define options of type object
from sources:
- Programmatic or configuration files:
{"optionA": { foo: "foo", foo2: ["1", 2 ]}}
- Environment variables:
MODULE_NAME_OPTION_A={"foo":"foo","foo2":["1",2]}}
- Arguments:
node myProgram.js --optionA={"foo":"foo","foo2":["1",2]}}
Array
Options of type array
are parsed from JSON strings when they are defined in environment variables. For defining them in command line arguments, multiple arguments should be provided. As in the case of objects, arrays are merged when they are defined in multiple sources when loading the configuration. The result of defining it environment variables and in arguments would be both arrays concated, for example. This behavior can be disabled using the mergeArrays
option of the Config
class.
const option = config.addOption({
name: "optionA",
type: "array",
itemsType: "string"
});
await config.load();
Examples about how to define options of type object
from sources:
- Programmatic or configuration files:
{"optionA": ["foo","foo2"]}
- Environment variables:
MODULE_NAME_OPTION_A=["foo","foo2"]
- Arguments: A commander variadic option is created under the hood to get the values, so array items have to be defined in separated arguments. Read the commander docs for further info.
node myProgram.js --optionA foo foo2
The contents of the array are also converted to its correspondent type when the itemsType
option is provided.
Built-in options
The library registers some options that can be used to determine the behavior of the library itself. As the rest of the configuration created by the library, these options can be set using configuration file, environment variables, command line arguments, etc. But there are some of them that can be defined only in some specific sources because they affect to reading that sources or not.
All of the built-in options are defined in the config
namespace:
config.readFile
(Boolean): Default: true
. Determines whether the configuration file should be read or not. Obviously, it would be ignored if it is defined in the configuration file.config.readArguments
(Boolean): Default: true
. Determines whether the arguments are read or not. It can be defined only using programmatic configuration.config.readEnvironment
(Boolean): Default: true
. Determines whether the environment variables are read or not. It can be defined using programmatic configuration or command line arguments.config.fileSearchPlaces
(Array): Default from cosmiconfig. An array of places to search for the configuration file. It can be defined in any source, except configuration files.config.allowUnknownArguments
(Boolean): Default false
. When set to true
, it allows to define unknown command line arguments. It can be defined in any source.
Lifecycle
Once the config
instance has been created, normally you should only call to the config.load
method to load all of the configuration, apply the validations and let the library start emitting events. But for more complex use cases there is available also another method: config.init
.
You can call to the config.init
method at any time before calling to config.load
, and all configuration sources will be preloaded and values will be assigned to the options that have been already created. In this step, the validation is not executed, so you can add more namespaces or options based on some of that configuration afterwards. For example, Mocks Server first load the configuration that defines the plugins to be loaded using the init
method, then it loads them and let them add their own options, and then it executes the config.load
method. In this step the validation is executed, so unknown configuration properties would produce an error. Option events are not emitted until the load
method has finished.
API
Config
const config = new Config({ moduleName: "mocks", mergeArrays: false });
Config instance
init(programmaticConfig)
: Async. Read configuration and assign it to the correspondent options but do not execute validation. Allows to read partial configuration before adding more namespaces or options. Events are not still emitted.
programmaticConfig
(Object): Optional. Initial configuration to be set in options. It overwrite option defaults, but it is overwritten by config from files, environment and arguments.
load(programmaticConfig)
: Async. Assign configuration to the correspondent options. It executes the init
method internally if it was not done before.
programmaticConfig
(Object): Optional. If init
was called before, it is ignored, otherwise, it is passed to the init
method.
addNamespace(name)
: Add namespace to the root. Returns a namespace instance.
name
(String): Name for the namespace.
addOption(optionProperties)
: Equivalent to the addOption
method in namespaces, but it add the option to the root. Returns an option instance.
optionProperties
(Object): Properties defining the option. See the addOption
method in namespaces for further info.
addOptions(optionsProperties)
: Add many options. Returns an array of option instances.
optionsProperties
(Array): Array of optionProperties
.
namespace(name)
: Returns the namespace instance in the root config with name equal to name
.option(optionName)
: Returns the option instances in the root config with name equal to optionName
.set(configuration)
: Set configuration properties to each correspondent namespace and options.
configuration
(Object): Object with programmatic configuration. Levels in the object correspond to namespaces names, and last level keys correspond to option names.
validate(configuration, options)
: Allows to prevalidate a configuration before setting it, for example. It returns an object with valid
and errors
properties. See AJV docs for further info.
configuration
(Object): Object with configuration. Levels in the object correspond to namespaces names, and last level keys correspond to option names.options
(Object): Object with extra options for validation:
allowAdditionalProperties
(Boolean): Default false
. If true, additional properties in the configuration would not produce validation errors.
getValidationSchema(options)
: Returns a validation schema compatible with AJV for validating the configuration of all nested namespaces.
options
(Object): Object with extra options for validation:
allowAdditionalProperties
(Boolean): Default false
. If true, the validation schema will allow additional properties.
value
: Getter returning the current values from all namespaces and options as an object. Levels in the object correspond to namespaces names, and last level keys correspond to option names. It can be also used as setter as an alias of the set
method, with default options.loadedFile
: Getter returning the file path of the loaded configuration file. It returns null
if no configuration file was loaded.namespaces
: Getter returning array with all root namespaces.options
: Getter returning array with all root options.programmaticLoadedValues
: Getter returning initial values from programmatic config. Useful for debugging purposes.fileLoadedValues
: Getter returning initial values from file config. Useful for debugging purposes.envLoadedValues
: Getter returning initial values from environment config. Useful for debugging purposes.argsLoadedValues
: Getter returning initial values from arguments. Useful for debugging purposes.
Namespace instance
const namespace = config.addNamespace("name");
addNamespace(name)
: Add another namespace to the current namespace. Returns a namespace instance.
name
(String): Name for the namespace.
addOption(optionProperties)
: Adds an option to the namespace. Returns an option instance.
optionProperties
(Object): Properties defining the option.
name
(String): Name for the option.description
(String): Optional. Used in help, traces, etc.type
(String). One of string
, boolean
, number
, array
or object
. Used to apply type validation when loading configuration and in option.value
setter.itemsType
(String). Can be defined only when type
is array
. It must be one of string
, boolean
, number
or object
.default
- Optional. Default value. Its type depends on the type
option.extraData
- (Object). Optional. Useful to store any extra data you want in the option. For example, Mocks Server uses it to define whether an option must be written when creating the configuration scaffold or not.
addOptions(optionsProperties)
: Add many options. Returns an array of option instances.
optionsProperties
(Array): Array of optionProperties
.
namespace(name)
: Returns the namespace instance in this namespace with name equal to name
.option(optionName)
: Returns the option instances in this namespace with name equal to optionName
.name
: Getter returning the namespace name.namespaces
: Getter returning an array with children namespaces.options
: Getter returning an array object with children options.set(configuration)
: Set configuration properties to each correspondent child namespace and options.
configuration
(Object): Object with configuration. Levels in the object correspond to child namespaces names, and last level keys correspond to option names.
value
: Getter returning the current values from all child namespaces and options as an object. Levels in the object correspond to namespaces names, and last level keys correspond to option names. It can be also used as setter as an alias of the set
method, with default options.root
: Getter returning the root configuration object.
Option instance
const option = namespace.addOption("name");
const rootOption = config.addOption("name2");
value
: Getter of the current value. It can be also used as setter as an alias of the set
method, with default options..set(value)
: Set value.onChange(callback)
: Allows to add a listener that will be executed whenever the value changes. It only emit events after calling to the config.start
method. It returns a function that removes the listener once executed.
callback(value)
(Function): Callback to be executed whenever the option value changes. It receives the new value as first argument.
name
: Getter returning the option name.type
: Getter returning the option type.description
: Getter returning the option description.extraData
: Getter returning the option extra data.default
: Getter returning the option default value.hasBeenSet
: Returns true if the option value has been actively set, no matter the source or method used to set it. Otherwise returns false.