2.0.1 • Published 2 years ago

ezyconfig v2.0.1

Weekly downloads
-
License
ISC
Repository
github
Last release
2 years ago

ezyconfig

CircleCI

An environment variable based configuration loader for node projects. The motivation for this project is to minimise magic, improve communication and documentation in the form of self documenting environment variables and to encourage best (and common) practices around config loading, validation and consistency between different environments.

There are three distinct steps to using this module:

Definition

All configurations should be defined as a single function that returns an object using the variable builders provided to the config builder function. The variable builders should be used to define the type of the environment variable and any validators that should be used to ensure that the variable is of the correct type and format.

Compilation

The compilation stage is where we take the config builder function and compile it down into a real object. It is at this stage that we run the type coercions, validators and transformer functions. Any malformed or missing environment variables will throw an exception and will be immediately obvious as soon as the service starts, rather than at some point during service execution. After a config has been compiled, the developer can be sure of the types and format of the loaded configuration, freeing them up to build real business value without the concern of type casting/validation etc.

Runtime

The returned config object from the ConfigBuilder is an object that has been wrapped in proxies to provide more detailed error messages and to ensure that the loaded config remains immutable, avoiding any potentially strange behaviour.

Sometimes it is necessary to get the normal config object that has not been wrapped in a proxy so that you can pass the object down into dependencies without causing exceptions when accessing non-existent properties. To do this, you can call toJSON at any level in the config object and this will return the "real" object. Example:

const ExternalLib = require("some-external-library");

const lib = new ExternalLib(config.externalLibConfig.toJSON());

lib.doSomething();

Support Utilities / Scripts

Because we have taken a declarative approach to config definition, we are able to parse the config and compute a list of required environment variables including their type, key, default value, validators etc. This means that we can simply pass the config function to the script and pass the output to the pipeline developer to configure the individual environments.

You should install this module globally to use this script:

npm i -g ezyconfig

The script usage is as follows:

explain-config [options] <configFile> [plugAndPlayFiles...]

You have several output options available:

OptionFormat
-o tableA formatted table that is useful for copying into markdown, such as an ID Card
-o jsonA JSON stringified format of the environment variables
-o ymlKey/Value pairs in YML format for copying into service charts

Plug and Play Environments

It is very common for multiple services to use the same resources, for example kafka, mongo, sql etc. but we might not want to force the developers of these services to keep the same config format. To support this, we have a concept of plug and play environments, where you can define a shared set of environment variables to inject into the config builder function. The advantage of these plug and play environments are as follows:

  • Less risk of typos
  • Less boilerplate in service configs
  • Common environment variable names and formats across projects
  • The developer can still craft their config as they wish
  • Same validation as config value builders

Example:

module.exports = (env, {kafka, mongo}) => ({
   mongo: {
       database: "my-project-db",
       host: mongo.host,
       user: mongo.user,
       password: mongo.password
   },
   kafka: {
       ...kafka
   },
   other: {
       variable: env.value("PROJECT_ENV_VAR", true).asBoolean()
   }
});

There are some default plug and play environments available in this module. They can be loaded like so:

const {mongo, kafka, azure, launchDarkly} = require("ezyconfig");
const {ConfigBuilder} = require("ezyconfig");

const builder = new ConfigBuilder();

builder
    .loadPlugAndPlayEnv(mongo)
    .loadPlugAndPlayEnv(kafka)
    .loadPlugAndPlayEnv(azure)
    .loadPlugAndPlayEnv(launchDarkly);

Usage

Please see the examples or tests folder for concrete examples of how to use this module. For brevity, we have listed some common examples below:

Declaring a secret value

module.exports = (env) => ({
    secretValue: env.secret("SECRET_ENV_VAR")
});

Declaring an ENV var with a default value

module.exports = (env) => ({
    someValue: env.value("ENV_VAR_NAME", "default-value")
});

Parsing into specified types

module.exports = (env) => ({
    boolValue: env.value("BOOL_ENV_VAR").asBoolean(), // BOOL_ENV_VAR=(1|0|true|false|TRUE|FALSE)
    intValue: env.value("INT_VAR").asNumber(), // INT_VAR=(1, 2, 3, ...)
    jsonObject: env.value("JSON_STRING").asObject(), // JSON_STRING={"some": "json"}
    timePeriod: env.value("TIME_PERIOD_VALUE").asInterval(), // TIME_PERIOD_VALUE=(5 seconds, 2 years, ...)
    
    // Arrays of values
    boolValueArray: env.value("ARR_BOOL_ENV_VAR").asArray(",").ofBooleans(), // ARR_BOOL_ENV_VAR=true, true, false, 0
    intValueArray: env.value("ARR_INT_VAR").asArray("|").ofNumbers(), // ARR_INT_VAR=1|2|656|4
    jsonObjectArray: env.value("ARR_JSON_STRING").asArray("|").ofObjects(), // ARR_JSON_STRING={"some": "json"}|{"hello": "world}
    timePeriodArray: env.value("ARR_TIME_PERIOD_VALUE").asArray(",").ofIntervals(), // ARR_TIME_PERIOD_VALUE=5 seconds, 2 years
});

Validating parsed values

You can provide custom validators to the validate function in the form of:

{
    name: "validatorName", // Used to provide helpful error messages
    fn: (value) => /^[a-z]$/.test(value) // Return true if the value passes validation otherwise return false
}

Otherwise, you can make use of the set of validators provided in this library:

ValidatorDescription
isAlphaValidates that the value contains only alpha chars
isAlphanumericValidates that the value contains only alphanumeric chars
isAsciiValidates that the string contains ASCII chars only
isBase32Validates that the string is base32 encoded
isBase64Validates that the string is base64 encoded
isBICValidates that the string is a BIC (Bank Identification Code) or SWIFT code
isBtcAddressValidates that the string is a bitcoin address
isCurrencyValidates that the string is a valid currency amount
isDataURIValidates that the string is a valid data URI format
isDateValidates that the string is a valid date
isDecimalValidates that the string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc
isDivisibleByValidates that the string is a number that's divisible by another
isEANValidates that the string is an EAN (European Article Number)
isEmailValidates that the string is a valid email address
isFQDNValidates that the string is a fully qualified domain name
isHexadecimalValidates that the string is a hexadecimal number
isHexColorValidates that the string is a hexadecimal color
isHSLValidates that the string is an HSL (hue, saturation, lightness, optional alpha) color based on CSS Colors Level 4 specification
isIBANValidates that the string is a IBAN (International Bank Account Number)
isIPValidates that the string is an IP (version 4 or 6)
isIPRangeValidates that the string is an IP Range (version 4 or 6)
isISO8601Validates that the string is a valid ISO 8601 date
isISO31661Alpha2Validates that the string is a valid ISO 3166-1 alpha-2 officially assigned country code
isISO31661Alpha3Validates that the string is a valid ISO 3166-1 alpha-3 officially assigned country code
isJWTValidates that the string is valid JWT token
isLocaleValidates that the string is a locale
isLowercaseValidates that the string is lowercase
isMACAddressValidates that the string is a MAC address
isMD5Validates that the string is a MD5 hash
isMimeTypeValidates that string matches to a valid MIME type format
isMongoIdValidates that the string is a valid hex-encoded representation of a MongoDB ObjectId
isMultibyteValidates that the string contains one or more multibyte chars
isNumericValidates that the string contains only numbers
isOctalValidates that the string is a valid octal number
isRFC3339Validates that the string is a valid RFC 3339 date
isRgbColorValidates that the string is a rgb or rgba color
isSemVerValidates that the string is a Semantic Versioning Specification (SemVer)
isUppercaseValidates that the string is uppercase
isSlugValidates that the string is of type slug
isURLValidates that the string is an URL
isUUIDValidates that the string is a UUID (version 3, 4 or 5)
isVariableWidthValidates that the string contains a mixture of full and half-width chars
isPortValidates that the value given is a valid port number
fileExistsValidates that the file path described exists - This is useful for service dependencies such as certificates etc

Example:

module.exports = (env) => ({
    servicePort: env.value("SERVICE_PORT").validate(env.validators.isPort),
    certificatePath: env.value("CERT_PATH").validate(env.validators.fileExists)
});

Typescript Support

From version 2.0.0 there is much better support for typescript with the parser declared type as the defined type on the built config. Example:

import ConfigBuilder from 'ezyconfig';

const config = ConfigBuilder((env) => ({
  bool: env.value('BOOL').asBoolean(),
  string: env.value('STR'),
  array: env.value('ARRAY').asArray(',').ofNumbers(),
  object: env.value('OBJ').asObject<{ hello: string }>(),
  fixedValue: true
}));

config.bool // Defined as readonly boolean
config.string // Defined as readonly string - default type
config.array // Defined as readonly number[]
config.object // Defined as the readonly value of the type specified in the generic, or Record<string, unknown> by default
config.fixedValue // Defined as readonly true

Default export

From version 2.0.0 there is a default config builder exported from the module. This is to reduce the amount of boilerplate needed when you want to just build the config and not inspect the loggable object etc.

2.0.1

2 years ago

1.1.3

3 years ago

1.1.2

3 years ago

1.1.1

3 years ago