@cirrusct/config

Config

Usage no npm install needed!

<script type="module">
  import cirrusctConfig from 'https://cdn.skypack.dev/@cirrusct/config';
</script>

README

config

Creates a configuration object of an arbitrary shape, merging multiple layers, including layers based on NODE_ENV setting. Settings can be optionally over-ridden by environment variable settings.

Also integrates with aws SSM Parameter Store, allowing SSM values to be used as source of environment variable overrides.

Basic Usage

Create your configuration

We'll use yaml here, but these files could be js (ES5 or 6), typescript or json. Or, you could pass the builder raw settings objects, or objects imported directly in code.

src/settings/base.yaml

setting1: mySetting
setting2: mySecondSetting

src/settings/development.yaml

setting1: myDevSetting

src/settings/env-vars.yaml

setting2: MYAPP_SETTING2

#shell

$ export NODE_ENV="development"
$ export MYAPP_SETTING2="my env setting"

Load configuration at run time

src/loadConfig.ts

import { createPackageConfigBuilder } from '@cirrusct/config';

// some run time base/default settings
const myDefaults = {
    setting0: 'internal default setting'
}

// create a builder with path info to our config settings
const builder = createPackageConfigBuilder(
    myDefaults, // pass defaults first, will be over-ridden if defined by imports
    {
        rootPath: 'src',
        baseName: 'settings'
    },
    // ... any number of objects to over-ride previous arguments
);

// build config
const settings = await builder.build();

// build() returns a single merged object based on source settings and environment

// outputs
console.log(settings.setting1);
  // -> $ myDevSetting
console.log(settings.setting2);
  // -> $ my env setting 
console.log(settings.setting0);
  // -> $ internal default setting

PackageConfigBuilder

PackageConfigBuilder handles merging (and optionally loading) of configuration settings. It takes various representations of the source settings and produces a single merged output object.

The constructor looks like this:

createPackageConfigBuilder = <T = {}>(...settings: PackageConfigBuilderArgs<T>[]): PackageConfigBuilder<T> => {
    return new PackageConfigBuilder<T>(...settings);
};

where T is the type of your final merged config object.

The constructor accepts any number of parameters representing source settings to be merged (with later arguments overriding prior ones).

Each argument can be of one of the following types:

A PackageConfigSettings Object

A PackageConfigSettings object with the following shape:

export interface PackageConfigSettings<TConfig = {}> {
    base: TConfig;
    nodeEnv?: { [node_env: string]: TConfig };
    envVars?: any; // all values are string names, but shape should match TConfig
}

base: default version of the final output object

nodeEnv: a map of the final output object keyed on NODE_ENV environment variable values, where values defined will be merged over base values

envVars: an object/JSON in the shape of the output object, with values containing environment variable names where the environment variable values for those names will be merged over base and nodeEnv values.

A Literal Object of the output Type

An argument can be an instance of the output object itself. Useful if there are no NODE_ENV specific settings, or as a set of values to provide defaults or over-rides for other PackageConfigSettings passed to the constructor.

A PackageConfigPathOptions Object

This argument refers to a file or directory containing configuration settings to be loaded by the builder. PackageConfigPathOptions looks like this:

export interface PackageConfigPathOptions {
    rootPath: string;
    baseName?: string;
    isRequired?: boolean;
}

PackagePathOptions Arguments

rootPath: Name of a supported configuration file

baseName (optional): If rootPath is a directory, baseName is required to indicate the name of a sub directory or a supported file name under the rootPath from which to read config. The builder will attempt to find files with the baseName and a supported extension directly under rootPath

isRequired: Flag indicating whether an error should be raised if configurations are not found, as there are use cases where configuration files are optional.

PackagePathOptions Supported Configuration Files

Configuration files can be defined in several formats to be directly imported by the builder (via an argument in the form of the PackageConfigPathOptions):

typescript: entry files with .ts extensions, transpiled by Typescript

ES6: entry files with .js extensions, transpiled by Babel

ES5: entry files with .js extensions, using traditional Node module.exports

json: standard json

yaml: standard yaml

Regardless of how configurations are defined, its is expected that each source will return an object with either the shape of your final configuration object, or in the shape PackageConfigSettings (which allows settings defined in above files to be merged.

PackagePathOptions File Resolution

When importing file-based configuration (via a PackageConfigPathOptions argument), the following rules are used to resolve files:

  1. If rootPath is a valid file, it is imported directly
  2. if rootPath is a directory, baseName is used to search first to search for a file with that name and a supported extension. If found, that file will be imported. If no files exist with baseName + supported extension, the importer will look for a directory with baseName.
  3. For directory imports, the importer will first look for index.(js|ts), and try to load that. If no index.(js|ts), importer will look for separate files named based on the structure of PackageConfigSettings (ie base.(yaml|json), files named based on NODE_ENV setting, and a file named env-vars.(yaml|json), etc.). These files will be loaded and assembled into a PackageConfigSettings object, and the values will be merged as described above.

PackagePathOptions References to other config files

For settings defined in files and passed to the builder through PackageConfigPathOptions argument, external config settings can be referenced inline using the syntax $ref= where is relative to the current file. can reference a directory containing standard files (base, nodEnv, envOverride), or can be a path to a specific configuration file in a supported format.

src/build.yaml

buildSetting1: buildSetting
appSettings: $ref=./app/settings

src/app/settings/base.yaml

setting1: myAppSetting
setting2: myAppSetting2

src/loadConfig.ts

import { createPackageConfigBuilder } from '@cirrusct/config';

// create a builder with path info to our config settings
const builder = createPackageConfigBuilder(
    {
        rootPath: 'src/build.yaml',
    }
);

// build config
const settings = await builder.build();

// outputs
console.dir(settings.appSettings);
  // -> $ { setting1: myAppSetting, setting2: myAppSetting2 }

AWS Parameter Store Integration

AWS Parameter store can be used to hold values and is essentially used as a source for environment variable overrides.

Current implementation is very limited. It looks for an environment variable value for PACKAGE_CONFIG_SSM_PARAMETER. The value of this environment variable defines the name of the SSM parameter to lookup. The value of this parameter in SSM is expected to be a JSON object containing keys with environment variable names defined in envVars of the configuration and values that will be used to override the setting its mapped to. The SSM values for each envOverride will be merged on top of the configuration settings.

For example:

## Define the name of the SSM parameter to pull env var vals from
export PACKAGE_CONFIG_SSM_PARAMETER="mySsmParam"

src/settings/env-vars.yaml

## Define the env var names that map to a setting
setting1: MYAPP_SETTING1

AWS SSM mySsmParam value

{
    "MYAPP_SETTING1": "SSM setting1 value"
}

setting1 gets its value from SSM