@creditkarma/dynamic-config
Advanced tools
Comparing version 0.2.0 to 0.3.3
@@ -6,2 +6,29 @@ # Change Log | ||
<a name="0.3.1"></a> | ||
## [0.3.1](https://github.com/creditkarma/thrift-server/compare/v0.3.0...v0.3.1) (2018-01-02) | ||
### Bug Fixes | ||
* Fix lerna publish ([#36](https://github.com/creditkarma/thrift-server/issues/36)) ([53571eb](https://github.com/creditkarma/thrift-server/commit/53571eb)) | ||
<a name="0.3.0"></a> | ||
# [0.3.0](https://github.com/creditkarma/thrift-server/compare/v0.2.0...v0.3.0) (2018-01-02) | ||
### Bug Fixes | ||
* **dynamic-config:** Support default exports from JS or TS ([#35](https://github.com/creditkarma/thrift-server/issues/35)) ([55e525a](https://github.com/creditkarma/thrift-server/commit/55e525a)) | ||
### Features | ||
* **dynamic-config:** Allow js and ts files as config sources ([#34](https://github.com/creditkarma/thrift-server/issues/34)) ([292855a](https://github.com/creditkarma/thrift-server/commit/292855a)) | ||
<a name="0.2.0"></a> | ||
@@ -8,0 +35,0 @@ # [0.2.0](https://github.com/creditkarma/thrift-server/compare/v0.1.3...v0.2.0) (2017-12-19) |
@@ -52,2 +52,43 @@ "use strict"; | ||
} | ||
function loadFileWithName(configPath, name) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const configs = yield utils.valuesForPromises(constants_1.SUPPORTED_FILE_TYPES.map((ext) => { | ||
const filePath = path.resolve(configPath, `${name}.${ext}`); | ||
return readFile(filePath).then((content) => { | ||
switch (ext) { | ||
case 'js': { | ||
const configObj = require(filePath); | ||
if (typeof configObj.default === 'object') { | ||
return configObj.default; | ||
} | ||
else { | ||
return configObj; | ||
} | ||
} | ||
case 'ts': { | ||
require('ts-node').register({ | ||
lazy: true, | ||
compilerOptions: { | ||
allowJs: true, | ||
rootDir: '.', | ||
}, | ||
}); | ||
const configObj = require(filePath); | ||
if (typeof configObj.default === 'object') { | ||
return configObj.default; | ||
} | ||
else { | ||
return configObj; | ||
} | ||
} | ||
default: | ||
return parseContent(content); | ||
} | ||
}, (err) => { | ||
return {}; | ||
}); | ||
})); | ||
return utils.overlayObjects(...configs); | ||
}); | ||
} | ||
class ConfigLoader { | ||
@@ -60,7 +101,3 @@ constructor({ configPath = constants_1.DEFAULT_CONFIG_PATH, configEnv } = {}) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return readFile(path.resolve(this.configPath, 'default.json')).then((content) => { | ||
return parseContent(content); | ||
}, (err) => { | ||
return {}; | ||
}); | ||
return loadFileWithName(this.configPath, 'default'); | ||
}); | ||
@@ -70,7 +107,3 @@ } | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return readFile(path.resolve(this.configPath, `${this.configEnv}.json`)).then((content) => { | ||
return parseContent(content); | ||
}, (err) => { | ||
return {}; | ||
}); | ||
return loadFileWithName(this.configPath, this.configEnv); | ||
}); | ||
@@ -77,0 +110,0 @@ } |
@@ -9,1 +9,2 @@ export declare const CONSUL_ADDRESS: string; | ||
export declare const CONFIG_SEARCH_PATHS: string[]; | ||
export declare const SUPPORTED_FILE_TYPES: string[]; |
@@ -13,2 +13,5 @@ "use strict"; | ||
]; | ||
exports.SUPPORTED_FILE_TYPES = [ | ||
'json', 'js', 'ts', | ||
]; | ||
//# sourceMappingURL=constants.js.map |
@@ -9,3 +9,3 @@ import { ConfigPlaceholder, IRemoteOverrides, ISchema } from './types'; | ||
export declare function setValueForKey<T>(key: string, value: any, oldObj: any): T; | ||
export declare function overlay<Base>(base: Base, update: Partial<Base>): Base; | ||
export declare function overlay<Base, Update>(base: Base, update: Update): Base & Update; | ||
export declare function overlayObjects<A, B, C, D, E>(one: A, two: B, three: C, four: D, five: E): A & B & C & D & E; | ||
@@ -29,1 +29,2 @@ export declare function overlayObjects<A, B, C, D>(one: A, two: B, three: C, four: D): A & B & C & D; | ||
export declare function race(promises: Array<Promise<any>>): Promise<any>; | ||
export declare function valuesForPromises(promises: Array<Promise<object>>): Promise<Array<object>>; |
@@ -75,2 +75,5 @@ "use strict"; | ||
} | ||
else if (key === '') { | ||
return value; | ||
} | ||
else { | ||
@@ -304,2 +307,6 @@ const newObj = (Array.isArray(oldObj)) ? [] : {}; | ||
} | ||
else if (obj instanceof Promise) { | ||
updates.push([path, obj]); | ||
return updates; | ||
} | ||
else if (typeof obj === 'object') { | ||
@@ -317,14 +324,29 @@ for (const key of Object.keys(obj)) { | ||
} | ||
function resolveObjectPromises(obj) { | ||
function handleUnresolved(unresolved, base) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const unresolved = collectUnresolvedPromises(obj); | ||
const paths = unresolved.map((next) => next[0].join('.')); | ||
const promises = unresolved.map((next) => next[1]); | ||
const resolvedPromises = yield Promise.all(promises); | ||
const resolvedPromises = yield Promise.all(promises.map((next) => { | ||
return next.then((val) => { | ||
const nested = collectUnresolvedPromises(val); | ||
if (nested.length > 0) { | ||
return handleUnresolved(nested, val); | ||
} | ||
else { | ||
return Promise.resolve(val); | ||
} | ||
}); | ||
})); | ||
const newObj = resolvedPromises.reduce((acc, next, currentIndex) => { | ||
return setValueForKey(paths[currentIndex], next, acc); | ||
}, obj); | ||
}, base); | ||
return newObj; | ||
}); | ||
} | ||
function resolveObjectPromises(obj) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const unresolved = collectUnresolvedPromises(obj); | ||
return handleUnresolved(unresolved, obj); | ||
}); | ||
} | ||
exports.resolveObjectPromises = resolveObjectPromises; | ||
@@ -401,2 +423,33 @@ function isValidRemote(name, resolvers) { | ||
exports.race = race; | ||
function processValues(values) { | ||
return values.sort((a, b) => { | ||
if (a[1] < b[1]) { | ||
return -1; | ||
} | ||
else { | ||
return 1; | ||
} | ||
}).map((next) => { | ||
return next[0]; | ||
}); | ||
} | ||
function resolveAtIndex(promise, index) { | ||
return new Promise((resolve, reject) => { | ||
promise.then((val) => { | ||
return resolve([val, index]); | ||
}, (err) => { | ||
return resolve([{}, index]); | ||
}); | ||
}); | ||
} | ||
function valuesForPromises(promises) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return Promise.all(promises.map((next, index) => { | ||
return resolveAtIndex(next, index); | ||
})).then((values) => { | ||
return processValues(values); | ||
}); | ||
}); | ||
} | ||
exports.valuesForPromises = valuesForPromises; | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "@creditkarma/dynamic-config", | ||
"version": "0.2.0", | ||
"version": "0.3.3", | ||
"description": "Dynamic Config for Node.js backed by Consul and Vault", | ||
@@ -16,8 +16,8 @@ "main": "dist/main/index.js", | ||
"docker:kill": "docker-compose kill", | ||
"pretest": "npm run build && npm run docker", | ||
"posttest": "npm run docker:kill", | ||
"pretest": "npm run build", | ||
"test": "npm run test:unit && npm run test:integration", | ||
"test:unit": "lab --timeout 10000 --verbose -l -S ./dist/tests/unit/*.spec.js", | ||
"pretest:integration": "cp -r ./src/tests/integration/config ./dist/tests/integration/config", | ||
"test:integration": "wait-on --timeout 20000 http://localhost:8510 http://localhost:8211 && node dist/tests/integration/bootstrap.js && lab --timeout 15000 --verbose -l -S ./dist/tests/integration/*.spec.js" | ||
"test:unit": "lab --timeout 10000 --verbose -l -S dist/tests/unit/*.spec.js", | ||
"pretest:integration": "rimraf dist/tests/integration/config && cp -r src/tests/integration/config dist/tests/integration/config && npm run docker", | ||
"test:integration": "wait-on --timeout 20000 http://localhost:8510 http://localhost:8211 && node dist/tests/integration/bootstrap.js && lab --timeout 15000 --verbose -l -S dist/tests/integration/*.spec.js", | ||
"posttest:integration": "npm run docker:kill" | ||
}, | ||
@@ -38,5 +38,6 @@ "author": "Credit Karma", | ||
"dependencies": { | ||
"@creditkarma/consul-client": "^0.2.0", | ||
"@creditkarma/vault-client": "^0.2.0" | ||
"@creditkarma/consul-client": "^0.3.3", | ||
"@creditkarma/vault-client": "^0.3.3", | ||
"ts-node": "^4.1.0" | ||
} | ||
} |
126
README.md
@@ -5,3 +5,3 @@ # Dynamic Config | ||
DynamicConfig supports local config in the form of JSON files and remote configuration for secrets and other independently deployed configuration. By default remote configuration is stored in Consul and secret config values are stored in Vault. There is also an API for registering other remote config sources. The use of remote configuration is optional. If these are not configurated only local config will be used. At least one local configuration file (`default.json`) is required. | ||
DynamicConfig supports local config in the form of JSON, JavaScript or TypeScript files. It also supports pulling configs from remote sources through a simple public API. By default remote configuration is stored in Consul and secret config values are stored in Vault. The use of remote configuration is optional. If these are not configurated only local config will be used. At least one local configuration file (`default.(json|js|ts)`) is required. | ||
@@ -20,3 +20,3 @@ ## Install | ||
When requesting a value from config a Promise of the expected result is returned. If the value is found the Promise is resolved. If the value is not found, either because it is missing or some other error, the Promise is rejected. | ||
When requesting a value from Dynamic Config a Promise of the expected result is returned. If the value is found the Promise is resolved. If the value is not found, either because it is missing or some other error, the Promise is rejected. | ||
@@ -75,5 +75,4 @@ #### Singleton | ||
export async function createHttpClient(): Promise<Client> { | ||
return config().getAll('hostName', 'port').then(([ host, port ]) => { | ||
return new Client(host, port) | ||
}) | ||
const [ host, port ] = await config().getAll('hostName', 'port') | ||
return new Client(host, port) | ||
} | ||
@@ -90,11 +89,97 @@ ``` | ||
The default config for your app is loaded from the `config/default.json` file. The default configuration is required. The default configuration is the contract between you and your application. | ||
The default config for your app is loaded from the `config/default.(json|js|ts)` file. The default configuration is required. The default configuration is the contract between you and your application. If there is both a `default.json` file and a `default.js` file the values from the `default.js` file will have presidence. | ||
#### Config Schema | ||
### File Types | ||
The three different file types are loaded in a predictable order. This means that if you have multiple files with the same base name but different extensions (`default.json` vs `default.ts`) the two files have different presidence based on their extension. JSON files are merged first, then JS and finally TS. This means that `ts` files have the highest presidence as their values are merged last. | ||
#### TypeScript | ||
Using TS files is convinient for co-locating your configs with the TypeScript interfaces for those configs. | ||
#### Exporting Values from TypeScript and JavaScript | ||
When exporting config values from a `ts` or `js` file you can either use named or default exports. | ||
Named exports: | ||
```typescript | ||
export const server = { | ||
hostName: 'localhost', | ||
port: 8080, | ||
} | ||
export const database = { | ||
username: 'root', | ||
password: 'root', | ||
} | ||
``` | ||
Default exports: | ||
```typescript | ||
export default { | ||
server: { | ||
hostName: 'localhost', | ||
port: 8080, | ||
}, | ||
database: { | ||
username: 'root', | ||
password: 'root', | ||
} | ||
} | ||
``` | ||
Either of these will add two keys to the compiled application config object. | ||
You can get at these values as: | ||
```typescript | ||
import { config } from '@creditkarma/dynamic-config' | ||
export async function createHttpClient(): Promise<Client> { | ||
const host: string = await config().get('server.hostName') | ||
const port: number = await config().get('server.port') | ||
return new Client(host, port) | ||
} | ||
``` | ||
#### Returning Promises | ||
When using `js` or `ts` configs your conifg values can be Promises. Dynamic Config will resolve all Promises while building the ultimate representation of your application config. | ||
As an example, this could be your local `js` config file: | ||
```typescript | ||
export const server = Promise.resolve({ | ||
hostName: 'localhost', | ||
port: 8080 | ||
}) | ||
``` | ||
Then when you fetch from Dynamic Config the Promise in your config is transparent: | ||
```typescript | ||
import { config } from '@creditkarma/dynamic-config' | ||
export async function createHttpClient(): Promise<Client> { | ||
const host: string = await config().get('server.hostName') | ||
const port: number = await config().get('server.port') | ||
return new Client(host, port) | ||
} | ||
``` | ||
Promises can also be nested, meaning keys within your returned config object can also have Promise values. Dynamic Config will recursively resolve all Promises before placing values in the resolved config object. | ||
This API can be used for loading config values from sources that don't neatly fit with the rest of the API. It does however make configs more messy and should ideally be used sparingly. We'll cover how to get values from remote sources in a more organized fashion shortly. | ||
Note: If a nested Promise rejects the wrapping Promise also rejects and all values within the wrapping Promise are ignored. | ||
### Config Schema | ||
At runtime a schema (a subset of [JSON Schema](http://json-schema.org/)) is built from this default config file. You can only use keys that you define in your default config and they must have the same shape. Config values should be predictable. If the form of your config is mutable this is very likely the source (or result of) a bug. | ||
#### Local Overrides | ||
### Local Overrides | ||
You can override these values from the default config in a variety of ways, but they must follow the schema set by your default configuration file. Overwriting the default values is done by adding additional files corresponding to the value of `NODE_ENV`. For example if `NODE_ENV = 'development'` then the default configuration will be merged with a file named `config/development.json`. Using this you could have different configuration files for `NODE_ENV = 'test'` or `NODE_ENV = 'production'`. | ||
You can override the values from the default config in a variety of ways, but they must follow the schema set by your default configuration file. Overwriting the default values is done by adding additional files corresponding to the value of `NODE_ENV`. For example if `NODE_ENV = 'development'` then the default configuration will be merged with a file named `config/development.(json|js|ts)`. Using this you could have different configuration files for `NODE_ENV = 'test'` or `NODE_ENV = 'production'`. | ||
@@ -125,5 +210,5 @@ ### Configuration Path | ||
### Registering a Remote Source | ||
### Remote Resolver | ||
Registering a remote source is fairly straight-forward. You use the `register` method on your config instance. | ||
Registering a remote resolver is fairly straight-forward. You use the `register` method on your config instance. | ||
@@ -332,3 +417,3 @@ Note: You can only register remote resolvers util your first call to `config.get()`. After this any attempt to register a resolver will raise an exception. | ||
#### Available Options | ||
### Available Options | ||
@@ -342,3 +427,3 @@ Here are the available options for DynamicConfig: | ||
#### Environment Variables | ||
### Environment Variables | ||
@@ -354,3 +439,3 @@ All options can be set through the environment. | ||
#### Constructor Options | ||
### Constructor Options | ||
@@ -392,6 +477,10 @@ They can also be set on the DynamicConfig constructor. | ||
#### Getting a Secret from Vault | ||
### Getting a Secret from Vault | ||
Getting a secret from Vault is similar to getting a value from local config or Consul. | ||
#### `getSecretValue` | ||
Will try to get a key from whatever remote is registered as a `secret` store. | ||
Based on the configuration the following code will try to load a secret from `http://localhost:8200/secret/username`. | ||
@@ -405,11 +494,8 @@ | ||
The method `getSecretValue` will try to get a key from whatever remote is registered as a `secret` store. | ||
### Config Placeholders | ||
#### Config Placeholders | ||
As mentioned config placeholders can also be used for `secret` stores. Review above for more information. | ||
### Roadmap | ||
## Roadmap | ||
* Support `.js` and `.ts` local config files | ||
* Add ability to watch a value for runtime changes | ||
@@ -416,0 +502,0 @@ * Explore options for providing a synchronous API |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
126135
1487
501
3
12
+ Addedts-node@^4.1.0
+ Added@creditkarma/consul-client@0.3.6(transitive)
+ Added@creditkarma/vault-client@0.3.6(transitive)
+ Added@types/strip-bom@3.0.0(transitive)
+ Added@types/strip-json-comments@0.0.30(transitive)
+ Addedansi-styles@3.2.1(transitive)
+ Addedarrify@1.0.1(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedchalk@2.4.2(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addeddiff@3.5.0(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedhas-flag@3.0.0(transitive)
+ Addedhomedir-polyfill@1.0.3(transitive)
+ Addedmake-error@1.3.6(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedparse-passwd@1.0.0(transitive)
+ Addedsource-map@0.6.1(transitive)
+ Addedsource-map-support@0.5.21(transitive)
+ Addedstrip-bom@3.0.0(transitive)
+ Addedstrip-json-comments@2.0.1(transitive)
+ Addedsupports-color@5.5.0(transitive)
+ Addedts-node@4.1.0(transitive)
+ Addedtsconfig@7.0.0(transitive)
+ Addedv8flags@3.2.0(transitive)
+ Addedyn@2.0.0(transitive)
- Removed@creditkarma/consul-client@0.2.0(transitive)
- Removed@creditkarma/vault-client@0.2.0(transitive)