@travetto/config
Advanced tools
| import { bulkRead, bulkReadSync, AppEnv, bulkFindSync } from '@travetto/base'; | ||
| import * as path from 'path'; | ||
| import * as yaml from 'js-yaml'; | ||
| import { readdirSync } from 'fs'; | ||
| import { ConfigMap } from './map'; | ||
| const ENV_SEP = '_'; | ||
| export class ConfigLoader { | ||
| private static _initialized: boolean = false; | ||
| private static map = new ConfigMap(); | ||
| static get(key: string) { | ||
| return this.map.get(key); | ||
| } | ||
| static bindTo(obj: any, key: string) { | ||
| return this.map.bindTo(obj, key); | ||
| } | ||
| /* | ||
| Order of specificity (least to most) | ||
| - Module configs -> located in the node_modules/@travetto/config folder | ||
| - Local configs -> located in the config folder | ||
| - External config file -> loaded from env | ||
| - Environment vars -> Overrides everything | ||
| */ | ||
| static initialize() { | ||
| if (this._initialized) { | ||
| return; | ||
| } | ||
| this._initialized = true; | ||
| if (!AppEnv.test) { | ||
| console.log(`Initializing: ${AppEnv.all.join(',')}`); | ||
| } | ||
| // Load all namespaces from core | ||
| let files = bulkReadSync([/^node_modules\/@travetto\/.*\/config\/.*[.]yml$/]); | ||
| // Load all configs, exclude env configs | ||
| files = files.concat(bulkReadSync([/^config\/.*[.]yml$/])); | ||
| for (const file of files) { | ||
| const ns = path.basename(file.name, '.yml'); | ||
| yaml.safeLoadAll(file.data, doc => this.map.putAll({ [ns]: doc })); | ||
| } | ||
| // Handle environmental loads | ||
| if (AppEnv.all.length) { | ||
| const loaded: string[] = []; | ||
| const envFiles = bulkReadSync([/^env\/.*[.]yml$/]) | ||
| .map(x => { | ||
| const tested = path.basename(x.name, '.yml'); | ||
| const found = AppEnv.is(tested); | ||
| return { name: tested, found, data: x.data }; | ||
| }) | ||
| .filter(x => x.found); | ||
| console.debug('Found configurations for', envFiles.map(x => x.name)); | ||
| for (const file of envFiles) { | ||
| yaml.safeLoadAll(file.data, doc => this.map.putAll(doc)); | ||
| } | ||
| } | ||
| // Handle process.env | ||
| for (const k of Object.keys(process.env)) { | ||
| if (k.includes(ENV_SEP)) { // Require at least one level | ||
| this.map.putFlattened(k.split(ENV_SEP), process.env[k] as string); | ||
| } | ||
| } | ||
| if (!process.env.QUIET_CONFIG && !AppEnv.test) { | ||
| console.log('Configured', this.map.toJSON()); | ||
| } | ||
| } | ||
| } |
| import { deepAssign, isPlainObject, isSimple } from '@travetto/base'; | ||
| type Prim = number | string | boolean | null; | ||
| type Nested = { [key: string]: Prim | Nested | Nested[] }; | ||
| function coerce(a: any, val: any): any { | ||
| if (a === 'null' && typeof val !== 'string') { | ||
| return null; | ||
| } | ||
| if (val === null || val === undefined) { | ||
| return a; | ||
| } | ||
| if (isSimple(val)) { | ||
| switch (typeof val) { | ||
| case 'string': return `${a}`; | ||
| case 'number': return `${a}`.indexOf('.') >= 0 ? parseFloat(`${a}`) : parseInt(`${a}`, 10); | ||
| case 'boolean': return (typeof a === 'string' && a === 'true') || !!a; | ||
| default: | ||
| throw new Error(`Unknown type ${typeof val}`); | ||
| } | ||
| } | ||
| if (Array.isArray(val)) { | ||
| return `${a}`.split(',').map(x => x.trim()).map(x => coerce(x, val[0])); | ||
| } | ||
| } | ||
| export class ConfigMap { | ||
| // Lowered, and flattened | ||
| private storage: Nested = {}; | ||
| putAll(data: Nested) { | ||
| deepAssign(this.storage, data, 'coerce'); | ||
| } | ||
| static getKeyName(key: string, data: { [key: string]: any }) { | ||
| key = key.trim(); | ||
| const match = new RegExp(key, 'i'); | ||
| const next = Object.keys(data).find(x => match.test(x)); | ||
| return next; | ||
| } | ||
| putFlattened(parts: string[], value: Prim) { | ||
| parts = parts.slice(0); | ||
| let key = parts.pop()!; | ||
| let data = this.storage; | ||
| if (!key) { | ||
| return false; | ||
| } | ||
| while (parts.length) { | ||
| const part = parts.shift()!; | ||
| const next = ConfigMap.getKeyName(part, data); | ||
| if (!next) { | ||
| return false; | ||
| } else { | ||
| data = data[next] as Nested; | ||
| } | ||
| } | ||
| if (!data) { | ||
| return false; | ||
| } | ||
| key = ConfigMap.getKeyName(key, data) || key; | ||
| data[key] = coerce(value, data[key]); | ||
| return true; | ||
| } | ||
| bindTo(obj: any, key: string) { | ||
| const keys = key.split('.'); | ||
| let sub: any = this.storage; | ||
| while (keys.length && sub[keys[0]]) { | ||
| sub = sub[keys.shift()!]; | ||
| } | ||
| return deepAssign(obj, sub); | ||
| } | ||
| get(key: string) { | ||
| return this.bindTo({}, key); | ||
| } | ||
| toJSON() { | ||
| return JSON.stringify(this.storage, null, 2); | ||
| } | ||
| } |
+1
-1
| export const init = { | ||
| priority: 0, | ||
| action: () => require('./src/service/config-loader').ConfigLoader.initialize() | ||
| action: () => require('./src/service/loader').ConfigLoader.initialize() | ||
| } |
+1
-3
@@ -8,5 +8,3 @@ { | ||
| "@travetto/base": "0.x.x", | ||
| "@types/flat": "0.0.28", | ||
| "@types/js-yaml": "^3.10.1", | ||
| "flat": "^4.0.0", | ||
| "js-yaml": "^3.11.0" | ||
@@ -22,3 +20,3 @@ }, | ||
| "scripts": {}, | ||
| "version": "0.0.14" | ||
| "version": "0.0.16" | ||
| } |
+38
-15
| travetto: Config | ||
| === | ||
| This module provides common infrastructure for application based initialization. There is a common | ||
| infrastructure pattern used: | ||
| - Configuration | ||
| - A scan for all `src/app/**/config.ts`. | ||
| - Each `config.ts` file must call `registerNamespace` to define a new configuration | ||
| namespace. | ||
| - Every configuration property can be overridden via environment variables (case-insensitive). | ||
| - Object navigation is separated by underscores | ||
| - e.g. `MAIL_TRANSPORT_HOST` would override `mail.transport.host` and specifically `transport.host` | ||
| in the `mail` namespace. | ||
| - All configuration variables should be loaded before any modules use it. | ||
| - `config.ts` should not require any code from your modules to ensure the order of loading | ||
| - Bootstrap | ||
| - Supports initializing the application, and then requiring classes using a glob pattern | ||
| to handle the common initialization process. | ||
| Common functionality for reading configuration from yaml files, and allowing overriding at execution time | ||
| - Process all config information: | ||
| - `node_modules/@travetto/*/config/*.yml` | ||
| - `config/*.yml` | ||
| - `env/<env>.yml` | ||
| - `process.env` (override only) | ||
| - Depending on which environments are specified, will selectively load `env/<env>.yml` files | ||
| - Every configuration property can be overridden via environment variables (case-insensitive). | ||
| - Object navigation is separated by underscores | ||
| - e.g. `MAIL_TRANSPORT_HOST` would override `mail.transport.host` and specifically `transport.host` | ||
| in the `mail` namespace. | ||
| Provides a decorator, `@Config("namespace")` that allows for classes to automatically bind config information | ||
| on post construct. The decorator will install a `postConstruct` method if not already defined. This is a hook | ||
| that is used by other modules. | ||
| ```typescript config.ts | ||
| @Config('sample') | ||
| class SampleConfig { | ||
| private host: string; | ||
| private port: number; | ||
| private creds = { | ||
| user: '', | ||
| password: '' | ||
| }; | ||
| } | ||
| ``` | ||
| And the corresponding config file | ||
| ```yaml app.yml | ||
| - sample | ||
| host: google.com | ||
| port: 80 | ||
| creds: | ||
| user: bob | ||
| password: bobspw | ||
| ``` |
@@ -1,2 +0,2 @@ | ||
| import { ConfigLoader } from '../service/config-loader'; | ||
| import { ConfigLoader } from '../service/loader'; | ||
@@ -3,0 +3,0 @@ export function Config(ns: string, depTarget?: new (...args: any[]) => any, name: string = '') { |
@@ -1,1 +0,2 @@ | ||
| export * from './config-loader'; | ||
| export * from './loader'; | ||
| export * from './map'; |
| import { Config, ConfigLoader } from '../src'; | ||
| import * as assert from 'assert'; | ||
@@ -17,4 +18,11 @@ class DbConfig { | ||
| if (conf.name !== 'Oscar') { | ||
| throw new Error('Should match!'); | ||
| } | ||
| assert(conf.name === 'Oscar'); | ||
| process.env.DB_MYSQL_NAME = 'Roger'; | ||
| delete (ConfigLoader as any)['_initialized']; | ||
| ConfigLoader.initialize(); | ||
| ConfigLoader.bindTo(conf, 'db.mysql'); | ||
| assert(conf.name === 'Roger'); |
| import { bulkRead, bulkReadSync, AppEnv, deepMerge, isPlainObject, bulkFindSync } from '@travetto/base'; | ||
| import * as flatten from 'flat'; | ||
| import * as yaml from 'js-yaml'; | ||
| import { EventEmitter } from 'events'; | ||
| import { readdirSync } from 'fs'; | ||
| const unflatten = flatten.unflatten; | ||
| type ConfigMap = { [key: string]: string | number | boolean | null | ConfigMap }; | ||
| export class ConfigLoader { | ||
| private static NULL = Symbol('NULL'); | ||
| private static data: ConfigMap = {}; | ||
| private static _initialized: boolean = false; | ||
| private static writeProperty(o: any, k: string, v: any) { | ||
| if (typeof v === 'string') { | ||
| if (typeof o[k] === 'boolean') { | ||
| v = v === 'true'; | ||
| } else if (typeof o[k] === 'number') { | ||
| v = v.indexOf('.') >= 0 ? parseFloat(v) : parseInt(v, 10); | ||
| } else if (typeof o[k] !== 'string' && v === 'null') { | ||
| v = this.NULL; | ||
| } else if (`${k}_0` in o) { // If array | ||
| v.split(/,\s*/g).forEach((el, i) => { | ||
| this.writeProperty(o, `${k}_${i}`, el); | ||
| }); | ||
| return; | ||
| } | ||
| } | ||
| o[k] = v; | ||
| } | ||
| private static merge(target: ConfigMap, source: ConfigMap, gentle = false) { | ||
| const targetFlat = flatten(target, { delimiter: '_' }) as any; | ||
| const sourceFlat = flatten(source, { delimiter: '_' }) as any; | ||
| // Flatten to lower case | ||
| const keyMap: { [key: string]: string } = {}; | ||
| const lowerFlat: ConfigMap = {}; | ||
| for (const k of Object.keys(targetFlat)) { | ||
| const lk = k.toLowerCase(); | ||
| lowerFlat[lk] = targetFlat[k]; | ||
| // handle keys, and all substrings | ||
| let end = k.length; | ||
| while (end > 0) { | ||
| const finalKey = lk.substring(0, end); | ||
| if (keyMap[finalKey]) { | ||
| break; | ||
| } else { | ||
| keyMap[finalKey] = k.substring(0, end); | ||
| end = k.lastIndexOf('_', end); | ||
| } | ||
| } | ||
| } | ||
| for (const k of Object.keys(sourceFlat)) { | ||
| const lk = k.toLowerCase(); | ||
| const ns = lk.split('_', 2)[0]; | ||
| if (!gentle || ns in target) { | ||
| if (!keyMap[lk]) { keyMap[lk] = k; } | ||
| this.writeProperty(lowerFlat, lk, sourceFlat[k]); | ||
| } | ||
| } | ||
| // Return original case | ||
| const out: ConfigMap = {}; | ||
| for (const k of Object.keys(lowerFlat)) { | ||
| out[keyMap[k]] = lowerFlat[k]; | ||
| } | ||
| deepMerge(target, unflatten(out, { delimiter: '_' })); | ||
| } | ||
| private static dropNulls(o: any) { | ||
| if (isPlainObject(o)) { | ||
| for (const k of Object.keys(o)) { | ||
| if (o[k] === this.NULL) { | ||
| delete o[k]; | ||
| } else { | ||
| this.dropNulls(o[k]); | ||
| } | ||
| } | ||
| } else if (Array.isArray(o)) { | ||
| o = o.map(this.dropNulls).filter((x: any) => x !== this.NULL); | ||
| } | ||
| return o; | ||
| } | ||
| static bindTo(obj: any, key: string) { | ||
| const keys = key.split('.'); | ||
| let sub: any = this.data; | ||
| while (keys.length && sub[keys[0]]) { | ||
| sub = sub[keys.shift()!]; | ||
| } | ||
| deepMerge(obj, sub); | ||
| return obj; | ||
| } | ||
| static get(key: string) { | ||
| return this.bindTo({}, key); | ||
| } | ||
| /* | ||
| Order of specificity (least to most) | ||
| - Module configs -> located in the node_modules/@travetto/config folder | ||
| - Local configs -> located in the config folder | ||
| - External config file -> loaded from env | ||
| - Environment vars -> Overrides everything | ||
| */ | ||
| static initialize() { | ||
| if (this._initialized) { | ||
| return; | ||
| } | ||
| this._initialized = true; | ||
| if (!AppEnv.test) { | ||
| console.log(`Initializing: ${AppEnv.all.join(',')}`); | ||
| } | ||
| // Load all namespaces from core | ||
| let files = bulkReadSync([/^node_modules\/@travetto\/.*\/config\/.*[.]yml$/]); | ||
| // Load all configs, exclude env configs | ||
| files = files.concat(bulkReadSync([/^config\/.*[.]yml$/])); | ||
| for (const file of files) { | ||
| const ns = file.name.split('/').pop()!.split('.yml')[0]; | ||
| yaml.safeLoadAll(file.data, doc => { | ||
| this.data[ns] = this.data[ns] || {}; | ||
| this.merge(this.data, { [ns]: doc }); | ||
| }); | ||
| } | ||
| if (AppEnv.all.length) { | ||
| const loaded: string[] = []; | ||
| const envFiles = bulkReadSync([/^env\/.*[.]yml$/]).reduce((acc, x) => { | ||
| const tested = x.name.split('/').pop()!.split('.yml')[0]; | ||
| const found = AppEnv.is(tested); | ||
| if (found) { | ||
| acc.push(x.data); | ||
| loaded.push(tested); | ||
| } | ||
| return acc; | ||
| }, [] as string[]); | ||
| console.debug('Found configurations for', loaded); | ||
| for (const file of envFiles) { | ||
| yaml.safeLoadAll(file, doc => { | ||
| this.merge(this.data, doc); | ||
| }); | ||
| } | ||
| } | ||
| // Handle process.env | ||
| this.merge(this.data, process.env as { [key: string]: any }, true); | ||
| // Drop out nulls | ||
| this.dropNulls(this.data); | ||
| if (!process.env.QUIET_CONFIG && !AppEnv.test) { | ||
| console.log('Configured', JSON.stringify(this.data, null, 2)); | ||
| } | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
3
-40%15
7.14%42
121.05%7626
-0.5%177
-1.12%8
33.33%1
Infinity%- Removed
- Removed
- Removed
- Removed
- Removed