diff --git a/packages/core/src/core/config/config.spec.ts b/packages/core/src/core/config/config.spec.ts index 53136c408c..7f14d2c11e 100644 --- a/packages/core/src/core/config/config.spec.ts +++ b/packages/core/src/core/config/config.spec.ts @@ -1,8 +1,9 @@ // std -import { strictEqual } from 'assert'; +import { strictEqual, throws } from 'assert'; +import { existsSync, mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs'; // FoalTS -import { existsSync, mkdirSync, rmdirSync, unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; import { Config } from './config'; import { ConfigNotFoundError } from './config-not-found.error'; import { ConfigTypeError } from './config-type.error'; @@ -12,396 +13,349 @@ function removeFile(path: string) { unlinkSync(path); } } + +const json = JSON.stringify({ + a: { + b: { + boolean: false, + booleanInString: 'false', + c: 1, + d: 'env(FOO_BAR)', + emptyString: ' ', + number: 1, + numberInString: '1', + string: 'hello world', + trueBooleanInString: 'true', + } + } +}); +const json2 = JSON.stringify({ + a: { + b: { + c: 2 + } + } +}); + +const yaml = `a: + b: + c: 1 + d: env(FOO_BAR) +`; +const yaml2 = `a: + b: + c: 2 +`; + +const js = 'module.exports = ' + json; +const js2 = 'module.exports = ' + json2; + describe('Config', () => { - beforeEach(() => Config.clearCache()); + beforeEach(() => { + Config.clearCache(); + + if (!existsSync('config')) { + mkdirSync('config'); + } + }); afterEach(() => { delete process.env.NODE_ENV; - delete process.env.BAR_FOO; - delete process.env.TEST_FOO_FOO_BAR; - delete process.env.TEST_FOO_FOO_BAR1; - delete process.env.TEST_FOO_FOO_BAR2; - delete process.env.TEST_FOO_FOO_BAR3; - delete process.env.TEST_FOO_FOO_BAR4; - delete process.env.TEST_FOO_FOO_BAR5; - delete process.env.TEST_FOO_FOO_BAR6; - delete process.env.TEST_FOO_FOO_BAR7; - delete process.env.DB_USERNAME; - - removeFile('.env'); + delete process.env.FOO_BAR; + + delete require.cache[join(process.cwd(), 'config/default.js')]; + delete require.cache[join(process.cwd(), 'config/development.js')]; + delete require.cache[join(process.cwd(), 'config/test.js')]; + removeFile('config/default.json'); removeFile('config/default.yml'); + removeFile('config/default.js'); removeFile('config/test.json'); removeFile('config/test.yml'); + removeFile('config/test.js'); removeFile('config/development.json'); removeFile('config/development.yml'); + removeFile('config/development.js'); if (existsSync('config')) { rmdirSync('config'); } + + Config.clearCache(); }); - describe('when get is called (static)', () => { + describe('when the static method "get" is called', () => { - it('should return the value of the environment variable if it exists.', () => { - process.env.TEST_FOO_FOO_BAR = 'value1'; - strictEqual(Config.get('test.foo.fooBar'), 'value1'); - }); + context('given the static method "set" has been called before', () => { - it('should return the value of the .env file if it exists (LF).', () => { - const fileContent = 'DB_HOST=localhost\nSETTINGS_SESSION_NAME=id\nFOO_BAR=a==\n'; - writeFileSync('.env', fileContent, 'utf8'); + beforeEach(() => Config.set('a.b.c', 2)); - strictEqual(Config.get('settings.sessionName'), 'id'); - strictEqual(Config.get('foo.bar'), 'a=='); - }); - - it('should return the value of the .env file if it exists (CRLF).', () => { - const fileContent = 'DB_HOST=localhost\r\nSETTINGS_SESSION_NAME=id\r\nFOO_BAR=a==\n'; - writeFileSync('.env', fileContent, 'utf8'); + it('should return the configuration value provided before.', () => { + strictEqual(Config.get('a.b.c'), 2); + }); - strictEqual(Config.get('settings.sessionName'), 'id'); - strictEqual(Config.get('foo.bar'), 'a=='); }); - it('should return, when NODE_ENV is defined, the value of the config/${NODE_ENV}.json file if it exists.', () => { - process.env.NODE_ENV = 'test'; - const fileContent = JSON.stringify({ - auth: { subSection: { key1: 'aaa' } } + function testConfigFile(path: string, fileContent: string, nodeEnv?: string): void { + beforeEach(() => { + writeFileSync(path, fileContent, 'utf8'); + if (nodeEnv) { + process.env.NODE_ENV = nodeEnv; + } }); - mkdirSync('config'); - writeFileSync('config/test.json', fileContent, 'utf8'); - - strictEqual(Config.get('auth.subSection.key1'), 'aaa'); - }); - it('should return, when NODE_ENV is defined, the value of the config/${NODE_ENV}.yml file if it exists.', () => { - process.env.NODE_ENV = 'test'; - const fileContent = 'hh:\n subSection:\n au: ji\n'; - mkdirSync('config'); - writeFileSync('config/test.yml', fileContent, 'utf8'); + it('should return the configuration value if the key exists.', () => { + strictEqual(Config.get('a.b.c'), 1); + }); - strictEqual(Config.get('hh.subSection.au'), 'ji'); - }); + it('should return the configuration value if the key exists (use of env(*)).', () => { + process.env.FOO_BAR = 'hello world'; + strictEqual(Config.get('a.b.d'), 'hello world'); + }); - it('should return, when NODE_ENV is not defined, the value of the config/development.json ' - + 'file if it exists.', () => { - const fileContent = JSON.stringify({ - a: 'b' + it('should return undefined if the key does not exist.', () => { + strictEqual(Config.get('unknown'), undefined); }); - mkdirSync('config'); - writeFileSync('config/development.json', fileContent, 'utf8'); + } - strictEqual(Config.get('a'), 'b'); + context('given NODE_ENV is defined and config/${NODE_ENV}.json exists', () => { + testConfigFile('config/test.json', json, 'test'); }); - it('should return, when NODE_ENV is not defined, the value of the config/development.yml ' - + 'file if it exists.', () => { - const ymlFileContent = 'c: d'; - mkdirSync('config'); - writeFileSync('config/development.yml', ymlFileContent, 'utf8'); + context('given NODE_ENV is not defined and config/development.json exists', () => { + testConfigFile('config/development.json', json); + }); - strictEqual(Config.get('c'), 'd'); + context('given NODE_ENV is defined and config/${NODE_ENV}.yml exists', () => { + testConfigFile('config/test.yml', yaml, 'test'); }); - it('should return the value of the config/default.json file if it exists.', () => { - const fileContent = JSON.stringify({ - jwt: { subSection: { secret: 'xxx' } } - }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); + context('given NODE_ENV is not defined and config/development.yml exists', () => { + testConfigFile('config/development.yml', yaml); + }); - strictEqual(Config.get('jwt.subSection.secret'), 'xxx'); + context('given NODE_ENV is defined and config/${NODE_ENV}.js exists', () => { + testConfigFile('config/test.js', js, 'test'); }); - it('should return the value of the config/default.yml file if it exists.', () => { - const fileContent = 'aa:\n subSection:\n wx: y\n'; - mkdirSync('config'); - writeFileSync('config/default.yml', fileContent, 'utf8'); + context('given NODE_ENV is not defined and config/development.js exists', () => { + testConfigFile('config/development.js', js); + }); - strictEqual(Config.get('aa.subSection.wx'), 'y'); + context('given config/default.json exists', () => { + testConfigFile('config/default.json', json); }); - it('should return undefined if the key does not exist and if no default value is provided.', () => { - strictEqual(Config.get('aa.bbbCcc.y'), undefined); + context('given config/default.yml exists', () => { + testConfigFile('config/default.yml', yaml); }); - it('should return the default value if the key does not exist.', () => { - strictEqual(Config.get('aa.bbbCcc.y', 'any', false), false); + context('given config/default.js exists', () => { + testConfigFile('config/default.js', js); }); - it('should look at the different values / files in the correct order.', () => { - process.env.NODE_ENV = 'test'; - mkdirSync('config'); + context('given multiple config files exist', () => { - const dotEnvFileContent = 'BAR_FOO=foo2'; - const envJSONFileContent = JSON.stringify({ barFoo: 'foo3' }); - const envYAMLFileContent = 'barFoo: foo4'; - const defaultJSONFileContent = JSON.stringify({ barFoo: 'foo5' }); - const defaultYAMLFileContent = 'barFoo: foo6'; + describe('should return the configuration value', () => { - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo7'); + it('with default.yml overriding default.js', () => { + writeFileSync('config/default.yml', yaml2); + writeFileSync('config/default.js', js); - writeFileSync('config/default.yml', defaultYAMLFileContent, 'utf8'); - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo6'); + strictEqual(Config.get('a.b.c'), 2); + }); - writeFileSync('config/default.json', defaultJSONFileContent, 'utf8'); - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo5'); + it('with default.json overriding default.yml', () => { + writeFileSync('config/default.json', json2); + writeFileSync('config/default.yml', yaml); - writeFileSync('config/test.yml', envYAMLFileContent, 'utf8'); - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo4'); + strictEqual(Config.get('a.b.c'), 2); + }); - writeFileSync('config/test.json', envJSONFileContent, 'utf8'); - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo3'); + it('with development.js overriding default.json (no NODE_ENV)', () => { + writeFileSync('config/development.js', js2); + writeFileSync('config/default.json', json); - writeFileSync('.env', dotEnvFileContent, 'utf8'); - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo2'); + strictEqual(Config.get('a.b.c'), 2); + }); - process.env.BAR_FOO = 'foo1'; - strictEqual(Config.get('barFoo', 'any', 'foo7'), 'foo1'); - }); + it('with development.yml overriding development.js (no NODE_ENV)', () => { + writeFileSync('config/development.yml', yaml2); + writeFileSync('config/development.js', js); - it('should, when type === "boolean", convert the configuration value to a boolean if possible.', () => { - process.env.TEST_FOO_FOO_BAR = 'true'; - const actual = Config.get('test.foo.fooBar', 'boolean'); - strictEqual(actual, true); + strictEqual(Config.get('a.b.c'), 2); + }); - process.env.TEST_FOO_FOO_BAR = 'false'; - const actual2 = Config.get('test.foo.fooBar', 'boolean'); - strictEqual(actual2, false); - }); + it('with development.json overriding development.yml (no NODE_ENV)', () => { + writeFileSync('config/development.json', json2); + writeFileSync('config/development.yml', yaml); - it('should, when type === "number", convert the configuration value to a number if possible.', () => { - process.env.TEST_FOO_FOO_BAR = '564'; - const actual = Config.get('test.foo.fooBar', 'number'); - strictEqual(actual, 564); - }); + strictEqual(Config.get('a.b.c'), 2); + }); - it('should, when type === "boolean|string", convert the configuration value to a boolean if possible.', () => { - process.env.TEST_FOO_FOO_BAR = 'true'; - const actual = Config.get('test.foo.fooBar', 'boolean|string'); - strictEqual(actual, true); + it('with values provided in "set" overriding development.json (no NODE_ENV)', () => { + Config.set('a.b.c', 2); + writeFileSync('config/development.json', json); - process.env.TEST_FOO_FOO_BAR = 'false'; - const actual2 = Config.get('test.foo.fooBar', 'boolean|string'); - strictEqual(actual2, false); - }); + strictEqual(Config.get('a.b.c'), 2); + }); + + }); + + it('should not delete configuration values (deep merge).', () => { + writeFileSync('config/development.json', json2); + writeFileSync('config/default.json', json); - it('should, when type === "number|string", convert the configuration value to a number if possible.', () => { - process.env.TEST_FOO_FOO_BAR = '46'; - let actual = Config.get('test.foo.fooBar', 'number|string'); - strictEqual(actual, 46); + strictEqual(Config.get('a.b.string'), 'hello world'); + }); - process.env.TEST_FOO_FOO_BAR = ' '; - actual = Config.get('test.foo.fooBar', 'number|string'); - strictEqual(actual, ' '); }); - it('should throw a ConfigTypeError if the configuration value does not have the expected type (string).', () => { - const fileContent = JSON.stringify({ - a: 'z', - b: 1, - c: true, + context('given no configuration value is found', () => { + + it('should return undefined if no default value is provided.', () => { + strictEqual(Config.get('a.b.c'), undefined); }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); - strictEqual(Config.get('a', 'string'), 'z'); + it('should return the default value if provided.', () => { + strictEqual(Config.get('a.b.c', 'any', 2), 2); + }); - try { - Config.get('b', 'string'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'b'); - strictEqual(error.expected, 'string'); - strictEqual(error.actual, 'number'); - } - - try { - Config.get('c', 'string'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'c'); - strictEqual(error.expected, 'string'); - strictEqual(error.actual, 'boolean'); - } }); - it('should throw a ConfigTypeError if the configuration value does not have the expected type (number).', () => { - const fileContent = JSON.stringify({ - a: 'z', - b: 1, - c: true, - d: ' ' + context('given a type is provided', () => { + + beforeEach(() => writeFileSync('config/default.json', json, 'utf8')); + + context('and type === "string"', () => { + + it('should throw a ConfigTypeError if the configuration value does not have the expected type.', () => { + strictEqual(Config.get('a.b.string', 'string'), 'hello world'); + + throws( + () => Config.get('a.b.number', 'string'), + new ConfigTypeError('a.b.number', 'string', 'number'), + ); + + throws( + () => Config.get('a.b.boolean', 'string'), + new ConfigTypeError('a.b.boolean', 'string', 'boolean'), + ); + }); + }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); - - try { - Config.get('a', 'number'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'a'); - strictEqual(error.expected, 'number'); - strictEqual(error.actual, 'string'); - } - - strictEqual(Config.get('b', 'number'), 1); - - try { - Config.get('c', 'number'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'c'); - strictEqual(error.expected, 'number'); - strictEqual(error.actual, 'boolean'); - } - - try { - Config.get('d', 'number'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'd'); - strictEqual(error.expected, 'number'); - strictEqual(error.actual, 'string'); - } - }); - it('should throw a ConfigTypeError if the configuration value does not have the expected type (boolean).', () => { - const fileContent = JSON.stringify({ - a: 'z', - b: 1, - c: true, + context('and type === "boolean"', () => { + + it('should convert the configuration value to a boolean if possible.', () => { + strictEqual(Config.get('a.b.booleanInString', 'boolean'), false); + strictEqual(Config.get('a.b.trueBooleanInString', 'boolean'), true); + }); + + it('should throw a ConfigTypeError if the configuration value does not have the expected type.', () => { + strictEqual(Config.get('a.b.boolean', 'boolean'), false); + + throws( + () => Config.get('a.b.number', 'boolean'), + new ConfigTypeError('a.b.number', 'boolean', 'number'), + ); + + throws( + () => Config.get('a.b.string', 'boolean'), + new ConfigTypeError('a.b.string', 'boolean', 'string'), + ); + }); + }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); - - try { - Config.get('a', 'boolean'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'a'); - strictEqual(error.expected, 'boolean'); - strictEqual(error.actual, 'string'); - } - - try { - Config.get('b', 'boolean'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'b'); - strictEqual(error.expected, 'boolean'); - strictEqual(error.actual, 'number'); - } - strictEqual(Config.get('c', 'boolean'), true); - }); + context('and type === "number"', () => { + + it('should convert the configuration value to a number if possible.', () => { + const actual = Config.get('a.b.numberInString', 'number'); + strictEqual(actual, 1); + }); + + it('should throw a ConfigTypeError if the configuration value does not have the expected type.', () => { + strictEqual(Config.get('a.b.number', 'number'), 1); + + throws( + () => Config.get('a.b.boolean', 'number'), + new ConfigTypeError('a.b.boolean', 'number', 'boolean'), + ); + + throws( + () => Config.get('a.b.string', 'number'), + new ConfigTypeError('a.b.string', 'number', 'string'), + ); + + throws( + () => Config.get('a.b.emptyString', 'number'), + new ConfigTypeError('a.b.emptyString', 'number', 'string'), + ); + }); - it('should throw a ConfigTypeError if the configuration value does not have the expected type (boolean|string).', - () => { - const fileContent = JSON.stringify({ - a: 'z', - b: 1, - c: true, }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); - strictEqual(Config.get('a', 'boolean|string'), 'z'); + context('and type === "boolean|string"', () => { - try { - Config.get('b', 'boolean|string'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'b'); - strictEqual(error.expected, 'boolean|string'); - strictEqual(error.actual, 'number'); - } + it('should convert the configuration value to a boolean if possible.', () => { + strictEqual(Config.get('a.b.booleanInString', 'boolean|string'), false); + strictEqual(Config.get('a.b.trueBooleanInString', 'boolean|string'), true); + }); - strictEqual(Config.get('c', 'boolean|string'), true); - }); + it('should throw a ConfigTypeError if the configuration value does not have the expected type.', () => { + strictEqual(Config.get('a.b.string', 'boolean|string'), 'hello world'); + strictEqual(Config.get('a.b.boolean', 'boolean|string'), false); + + throws( + () => Config.get('a.b.number', 'boolean|string'), + new ConfigTypeError('a.b.number', 'boolean|string', 'number'), + ); + }); - it('should throw a ConfigTypeError if the configuration value does not have the expected type (number|string).', - () => { - const fileContent = JSON.stringify({ - a: 'z', - b: 1, - c: true, }); - mkdirSync('config'); - writeFileSync('config/default.json', fileContent, 'utf8'); - strictEqual(Config.get('a', 'number|string'), 'z'); - strictEqual(Config.get('b', 'number|string'), 1); + context('and type === "number|string"', () => { + + it('should convert the configuration value to a number if possible.', () => { + const actual = Config.get('a.b.numberInString', 'number|string'); + strictEqual(actual, 1); + }); + + it('should throw a ConfigTypeError if the configuration value does not have the expected type.', () => { + strictEqual(Config.get('a.b.string', 'number|string'), 'hello world'); + strictEqual(Config.get('a.b.emptyString', 'number|string'), ' '); + strictEqual(Config.get('a.b.number', 'number|string'), 1); + + throws( + () => Config.get('a.b.boolean', 'number|string'), + new ConfigTypeError('a.b.boolean', 'number|string', 'boolean'), + ); + }); + + }); - try { - Config.get('c', 'number|string'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigTypeError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'c'); - strictEqual(error.expected, 'number|string'); - strictEqual(error.actual, 'boolean'); - } }); }); - describe('when getOrThrow is called', () => { + describe('when the static method "getOrThrow" is called', () => { it('should return the configuration value.', () => { - process.env.TEST_FOO_FOO_BAR = 'value1'; - strictEqual(Config.getOrThrow('test.foo.fooBar'), 'value1'); + writeFileSync('config/default.json', json, 'utf8'); + strictEqual(Config.getOrThrow('a.b.c'), 1); }); it('should throw a ConfigNotFoundError if the configuration key has no associated value.', () => { - try { - Config.getOrThrow('b'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigNotFoundError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'b'); - } - - try { - Config.getOrThrow('b', 'any', 'You must provide something.'); - throw new Error('An error should have been thrown'); - } catch (error) { - if (!(error instanceof ConfigNotFoundError)) { - throw new Error('The error should be an instance of ConfigTypeError.'); - } - strictEqual(error.key, 'b'); - strictEqual(error.msg, 'You must provide something.'); - } + throws( + () => Config.getOrThrow('unknown'), + new ConfigNotFoundError('unknown'), + ); + throws( + () => Config.getOrThrow('unknown', 'any', 'You must provide something.'), + new ConfigNotFoundError('unknown', 'You must provide something.') + ); }); }); diff --git a/packages/core/src/core/config/config.ts b/packages/core/src/core/config/config.ts index c24a19d6e1..749a4c9090 100644 --- a/packages/core/src/core/config/config.ts +++ b/packages/core/src/core/config/config.ts @@ -1,10 +1,11 @@ // 3p import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; // FoalTS import { ConfigNotFoundError } from './config-not-found.error'; import { ConfigTypeError } from './config-type.error'; -import { dotToUnderscore } from './utils'; +import { Env } from './env'; type ValueStringType = 'string'|'number'|'boolean'|'boolean|string'|'number|string'|'any'; @@ -46,31 +47,20 @@ export class Config { return defaultValue; } - if (type === 'boolean|string' && typeof value !== 'boolean') { + if (type === 'string' && typeof value !== 'string') { + throw new ConfigTypeError(key, 'string', typeof value); + } + + if (type === 'boolean' && typeof value !== 'boolean') { if (value === 'true') { return true as any; } if (value === 'false') { return false as any; } - if (typeof value !== 'string') { - throw new ConfigTypeError(key, 'boolean|string', typeof value); - } - } - if (type === 'number|string' && typeof value !== 'number') { - if (typeof value !== 'string') { - throw new ConfigTypeError(key, 'number|string', typeof value); - } - if (value.replace(/ /g, '') !== '') { - const n = Number(value); - if (!isNaN(n)) { - return n as any; - } - } - } - if (type === 'string' && typeof value !== 'string') { - throw new ConfigTypeError(key, 'string', typeof value); + throw new ConfigTypeError(key, 'boolean', typeof value); } + if (type === 'number' && typeof value !== 'number') { if (typeof value === 'string' && value.replace(/ /g, '') !== '') { const n = Number(value); @@ -80,14 +70,29 @@ export class Config { } throw new ConfigTypeError(key, 'number', typeof value); } - if (type === 'boolean' && typeof value !== 'boolean') { + + if (type === 'boolean|string' && typeof value !== 'boolean') { if (value === 'true') { return true as any; } if (value === 'false') { return false as any; } - throw new ConfigTypeError(key, 'boolean', typeof value); + if (typeof value !== 'string') { + throw new ConfigTypeError(key, 'boolean|string', typeof value); + } + } + + if (type === 'number|string' && typeof value !== 'number') { + if (typeof value !== 'string') { + throw new ConfigTypeError(key, 'number|string', typeof value); + } + if (value.replace(/ /g, '') !== '') { + const n = Number(value); + if (!isNaN(n)) { + return n as any; + } + } } return value; @@ -121,112 +126,106 @@ export class Config { * @memberof Config */ static clearCache() { - this.cache = { - dotEnv: undefined, - json: {}, - yaml: {}, - }; + this.config = null; + this.testConfig.clear(); } - private static yaml: any; - private static cache: { dotEnv: any, json: any, yaml: any } = { - dotEnv: undefined, - json: {}, - yaml: {}, - }; + static set(key: string, value: string|number|boolean): void { + this.testConfig.set(key, value); + } - private static readConfigValue(key: string): any { - const underscoreName = dotToUnderscore(key); + private static yaml: any; + private static config: { [key: string ]: any } | null = null; + private static testConfig: Map = new Map(); - const envValue = process.env[underscoreName]; - if (envValue !== undefined) { - return envValue; + private static readJSON(path: string): { [key: string ]: any } { + if (!existsSync(path)) { + return {}; } - const dotEnvValue = this.readDotEnvValue(underscoreName); - if (dotEnvValue !== undefined) { - return dotEnvValue; - } + const fileContent = readFileSync(path, 'utf8'); + return JSON.parse(fileContent); + } - const envJSONFilePath = `config/${process.env.NODE_ENV || 'development'}.json`; - const envJSONValue = this.readJSONValue(envJSONFilePath, key); - if (envJSONValue !== undefined) { - return envJSONValue; + private static readYAML(path: string): { [key: string ]: any } { + if (!existsSync(path)) { + return {}; } - const envYamlFilePath = `config/${process.env.NODE_ENV || 'development'}.yml`; - const envYAMLValue = this.readYAMLValue(envYamlFilePath, key); - if (envYAMLValue !== undefined) { - return envYAMLValue; + const yaml = this.getYAMLInstance(); + if (!yaml) { + console.log(`Impossible to read ${path}. The package "yamljs" is not installed.`); + return {}; } - const defaultJSONValue = this.readJSONValue('config/default.json', key); - if (defaultJSONValue !== undefined) { - return defaultJSONValue; - } + const fileContent = readFileSync(path, 'utf8'); + return yaml.parse(fileContent); + } - const defaultYAMLValue = this.readYAMLValue('config/default.yml', key); - if (defaultYAMLValue !== undefined) { - return defaultYAMLValue; + private static readJS(path: string): { [key: string ]: any } { + if (!existsSync(path)) { + return {}; } - } - private static readDotEnvValue(name: string): string | undefined { - if (!this.cache.dotEnv) { - if (!existsSync('.env')) { - return; - } + return require(join(process.cwd(), path)); + } - const envFileContent = readFileSync('.env', 'utf8'); - this.cache.dotEnv = {}; - envFileContent.replace(/\r\n/g, '\n').split('\n').forEach(line => { - const [ key, ...values ] = line.split('='); - const value = values.join('='); - this.cache.dotEnv[key] = value; - }); + private static readConfigValue(key: string): any { + if (this.testConfig.has(key)) { + return this.testConfig.get(key); } - if (this.cache.dotEnv[name] !== undefined) { - return this.cache.dotEnv[name]; + if (this.config === null) { + this.config = [ + this.readJS('config/default.js'), + this.readYAML('config/default.yml'), + this.readJSON('config/default.json'), + this.readJS(`config/${process.env.NODE_ENV || 'development'}.js`), + this.readYAML(`config/${process.env.NODE_ENV || 'development'}.yml`), + this.readJSON(`config/${process.env.NODE_ENV || 'development'}.json`), + ].reduce((config1, config2) => this.mergeDeep(config1, config2)); } - } - private static readJSONValue(path: string, key: string): any { - if (!this.cache.json[path]) { - if (!existsSync(path)) { - return; + const properties = key.split('.'); + let result: any = this.config; + for (const property of properties) { + result = result[property]; + if (result === undefined) { + break; } + } - const fileContent = readFileSync(path, 'utf8'); - this.cache.json[path] = JSON.parse(fileContent); + if (typeof result === 'string' && result.startsWith('env(') && result.endsWith(')')) { + const envVarName = result.substr(4, result.length - 5); + return Env.get(envVarName); } - return this.getValue(this.cache.json[path], key); + return result; } - private static readYAMLValue(path: string, key: string): any { - if (!this.cache.yaml[path]) { - if (!existsSync(path)) { - return; - } + private static mergeDeep(target: { [key: string]: any }, source: { [key: string]: any } ): { [key: string]: any } { + // TODO: improve the tests of this function. + function isObject(o: any): o is { [key: string]: any } { + return typeof o === 'object' && o !== null; + } - const yaml = this.getYAMLInstance(); - if (!yaml) { - console.log(`Impossible to read ${path}. The package "yamljs" is not installed.`); - return; + for (const key in source) { + if (isObject(target[key]) && isObject(source[key])) { + this.mergeDeep(target[key], source[key]); + } else { + target[key] = source[key]; } - - const fileContent = readFileSync(path, 'utf8'); - this.cache.yaml[path] = yaml.parse(fileContent); } - return this.getValue(this.cache.yaml[path], key); + return target; } private static getYAMLInstance(): false | any { + // TODO: test this method (hard). if (this.yaml === false) { return false; } + try { this.yaml = require('yamljs'); } catch (err) { @@ -238,16 +237,4 @@ export class Config { return this.yaml; } - private static getValue(config: any, propertyPath: string): any { - const properties = propertyPath.split('.'); - let result = config; - for (const property of properties) { - result = result[property]; - if (result === undefined) { - break; - } - } - return result; - } - } diff --git a/packages/core/src/core/config/utils.spec.ts b/packages/core/src/core/config/utils.spec.ts deleted file mode 100644 index c5f167c1a8..0000000000 --- a/packages/core/src/core/config/utils.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -// std -import { strictEqual } from 'assert'; - -// FoalTS -import { dotToUnderscore } from './utils'; - -describe('dotToUnderscore', () => { - - it('should properly convert dot keys to underscore keys.', () => { - const actual = dotToUnderscore('test.foo.fooBar.barFoo1'); - const expected = 'TEST_FOO_FOO_BAR_BAR_FOO1'; - - strictEqual(actual, expected); - }); - -}); diff --git a/packages/core/src/core/config/utils.ts b/packages/core/src/core/config/utils.ts index 90a2c88111..8d82941b6c 100644 --- a/packages/core/src/core/config/utils.ts +++ b/packages/core/src/core/config/utils.ts @@ -1,10 +1,3 @@ -export function dotToUnderscore(str: string): string { - return str - .replace(/([A-Z])/g, letter => `_${letter}`) - .replace(/\./g, '_') - .toUpperCase(); -} - function makeLine(str: string): string { const length = 58; const spacesAfter = length - str.length;