@travetto/config
Advanced tools
Comparing version 3.4.0-rc.3 to 3.4.0-rc.4
{ | ||
"name": "@travetto/config", | ||
"version": "3.4.0-rc.3", | ||
"version": "3.4.0-rc.4", | ||
"description": "Configuration support", | ||
@@ -29,5 +29,5 @@ "keywords": [ | ||
"dependencies": { | ||
"@travetto/di": "^3.4.0-rc.3", | ||
"@travetto/schema": "^3.4.0-rc.3", | ||
"@travetto/yaml": "^3.4.0-rc.3" | ||
"@travetto/di": "^3.4.0-rc.4", | ||
"@travetto/schema": "^3.4.0-rc.4", | ||
"@travetto/yaml": "^3.4.0-rc.4" | ||
}, | ||
@@ -34,0 +34,0 @@ "travetto": { |
@@ -23,7 +23,9 @@ <!-- This file was generated by @travetto/doc and should not be modified directly --> | ||
Config loading follows a defined resolution path, below is the order in increasing specificity (`ext` can be `yaml`, `yml`, `json`, `properties`): | ||
1. `resources/application.<ext>` - Load the default `application.<ext>` if available. | ||
1. `resources/*.<ext>` - Load profile specific configurations as defined by the values in `process.env.TRV_PROFILES` | ||
1. `resources/{env}.<ext>` - Load environment specific profile configurations as defined by the values of `process.env.TRV_ENV`. | ||
1. `resources/application.<ext>` - Priority `100` - Load the default `application.<ext>` if available. | ||
1. `resources/{env}.<ext>` - Priority `200` - Load environment specific profile configurations as defined by the values of `process.env.TRV_ENV`. | ||
1. `resources/*.<ext>` - Priority `300` - Load profile specific configurations as defined by the values in `process.env.TRV_PROFILES` | ||
By default all configuration data is inert, and will only be applied when constructing an instance of a configuration class. | ||
**Note**: When working in a monorepo, the parent resources folder will also be searched with a lower priority than the the module's specific resources. This allows for shared-global configuration that can be overridden at the module level. The general rule is that the longest path has the highest priority. | ||
### A Complete Example | ||
@@ -69,5 +71,13 @@ A more complete example setup would look like: | ||
sources: [ | ||
'application.1 - file://./doc/resources/application.yml', | ||
'prod.1 - file://./doc/resources/prod.json', | ||
'override.3 - memory://override' | ||
{ | ||
priority: 100, | ||
source: 'file://application', | ||
detail: 'module/config/doc/resources/application.yml' | ||
}, | ||
{ | ||
priority: 300, | ||
source: 'file://prod', | ||
detail: 'module/config/doc/resources/prod.json' | ||
}, | ||
{ priority: 999, source: 'memory://override' } | ||
], | ||
@@ -86,3 +96,3 @@ active: { | ||
import { ConfigData } from '../parser/types'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
@@ -93,19 +103,14 @@ /** | ||
export class MemoryConfigSource implements ConfigSource { | ||
priority = 1; | ||
data: Record<string, ConfigData>; | ||
name = 'memory'; | ||
priority: number; | ||
data: ConfigData; | ||
source: string; | ||
constructor(data: Record<string, ConfigData>, priority: number = 1) { | ||
constructor(key: string, data: ConfigData, priority: number = 500) { | ||
this.data = data; | ||
this.priority = priority; | ||
this.source = `memory://${key}`; | ||
} | ||
getValues(profiles: string[]): ConfigValue[] { | ||
const out: ConfigValue[] = []; | ||
for (const profile of profiles) { | ||
if (this.data[profile]) { | ||
out.push({ profile, config: this.data[profile], source: `${this.name}://${profile}`, priority: this.priority }); | ||
} | ||
} | ||
return out; | ||
getData(): ConfigData { | ||
return this.data; | ||
} | ||
@@ -117,5 +122,6 @@ } | ||
```typescript | ||
import { Env, GlobalEnv } from '@travetto/base'; | ||
import { Env } from '@travetto/base'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
import { ConfigData } from '../parser/types'; | ||
@@ -127,3 +133,3 @@ /** | ||
priority: number; | ||
name = 'env'; | ||
source: string; | ||
#envKey: string; | ||
@@ -134,11 +140,11 @@ | ||
this.priority = priority; | ||
this.source = `env://${this.#envKey}`; | ||
} | ||
getValues(profiles: string[]): ConfigValue[] { | ||
getData(): ConfigData | undefined { | ||
try { | ||
const data = JSON.parse(Env.get(this.#envKey, '{}')); | ||
return [{ profile: GlobalEnv.envName, config: data, source: `${this.name}://${this.#envKey}`, priority: this.priority }]; | ||
return data; | ||
} catch (e) { | ||
console.error(`env.${this.#envKey} is an invalid format`, { text: Env.get(this.#envKey) }); | ||
return []; | ||
} | ||
@@ -150,7 +156,7 @@ } | ||
### Custom Configuration Provider | ||
In addition to files and environment variables, configuration sources can also be provided via the class itself. This is useful for reading remote configurations, or dealing with complex configuration normalization. The only caveat to this pattern, is that the these configuration sources cannot rely on the [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L16) service for input. This means any needed configuration will need to be accessed via specific patterns. | ||
In addition to files and environment variables, configuration sources can also be provided via the class itself. This is useful for reading remote configurations, or dealing with complex configuration normalization. The only caveat to this pattern, is that the these configuration sources cannot rely on the [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L21) service for input. This means any needed configuration will need to be accessed via specific patterns. | ||
**Code: Custom Configuration Source** | ||
```typescript | ||
import { ConfigSource, ConfigValue } from '@travetto/config'; | ||
import { ConfigData, ConfigSource } from '@travetto/config'; | ||
import { Injectable } from '@travetto/di'; | ||
@@ -160,14 +166,7 @@ | ||
export class CustomConfigSource implements ConfigSource { | ||
priority = 1000; | ||
name = 'custom'; | ||
priority = 2000; | ||
source = 'custom://override'; | ||
async getValues(): Promise<ConfigValue[]> { | ||
return [ | ||
{ | ||
config: { user: { name: 'bob' } }, | ||
priority: this.priority, | ||
profile: 'override', | ||
source: `custom://${CustomConfigSource.name}` | ||
} | ||
]; | ||
async getData(): Promise<ConfigData> { | ||
return { user: { name: 'bob' } }; | ||
} | ||
@@ -178,6 +177,6 @@ } | ||
## Startup | ||
At startup, the [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L16) service will log out all the registered configuration objects. The configuration state output is useful to determine if everything is configured properly when diagnosing runtime errors. This service will find all configurations, and output a redacted version with all secrets removed. The default pattern for secrets is `/password|private|secret/i`. More values can be added in your configuration under the path `config.secrets`. These values can either be simple strings (for exact match), or `/pattern/` to create a regular expression. | ||
At startup, the [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L21) service will log out all the registered configuration objects. The configuration state output is useful to determine if everything is configured properly when diagnosing runtime errors. This service will find all configurations, and output a redacted version with all secrets removed. The default pattern for secrets is `/password|private|secret/i`. More values can be added in your configuration under the path `config.secrets`. These values can either be simple strings (for exact match), or `/pattern/` to create a regular expression. | ||
## Consuming | ||
The [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L16) service provides injectable access to all of the loaded configuration. For simplicity, a decorator, [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) allows for classes to automatically be bound with config information on post construction via the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module. The decorator will install a `postConstruct` method if not already defined, that performs the binding of configuration. This is due to the fact that we cannot rewrite the constructor, and order of operation matters. | ||
The [ConfigurationService](https://github.com/travetto/travetto/tree/main/module/config/src/service.ts#L21) service provides injectable access to all of the loaded configuration. For simplicity, a decorator, [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) allows for classes to automatically be bound with config information on post construction via the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module. The decorator will install a `postConstruct` method if not already defined, that performs the binding of configuration. This is due to the fact that we cannot rewrite the constructor, and order of operation matters. | ||
@@ -241,5 +240,13 @@ ### Environment Variables | ||
sources: [ | ||
'application.1 - file://./doc/resources/application.yml', | ||
'prod.1 - file://./doc/resources/prod.json', | ||
'override.3 - memory://override' | ||
{ | ||
priority: 100, | ||
source: 'file://application', | ||
detail: 'module/config/doc/resources/application.yml' | ||
}, | ||
{ | ||
priority: 300, | ||
source: 'file://prod', | ||
detail: 'module/config/doc/resources/prod.json' | ||
}, | ||
{ priority: 999, source: 'memory://override' } | ||
], | ||
@@ -246,0 +253,0 @@ active: { |
@@ -9,5 +9,10 @@ import util from 'util'; | ||
import { ConfigSourceTarget, ConfigTarget } from './internal/types'; | ||
import { ParserManager } from './parser/parser'; | ||
import { ConfigData } from './parser/types'; | ||
import { ConfigSource, ConfigValue } from './source/types'; | ||
import { ConfigSource } from './source/types'; | ||
import { FileConfigSource } from './source/file'; | ||
import { OverrideConfigSource } from './source/override'; | ||
type ConfigSpec = { source: string, priority: number, detail?: string }; | ||
/** | ||
@@ -19,17 +24,19 @@ * Manager for application configuration | ||
private static getSorted(configs: ConfigValue[], profiles: string[]): ConfigValue[] { | ||
const order = Object.fromEntries(Object.entries(profiles).map(([k, v]) => [v, +k] as const)); | ||
#storage: Record<string, unknown> = {}; // Lowered, and flattened | ||
#specs: ConfigSpec[] = []; | ||
#secrets: (RegExp | string)[] = [/secure(-|_|[a-z])|password|private|secret|salt|(api(-|_)?key)/i]; | ||
return configs.sort((left, right) => | ||
(order[left.profile] - order[right.profile]) || | ||
left.priority - right.priority || | ||
left.source.localeCompare(right.source) | ||
); | ||
async #toSpecPairs(cfg: ConfigSource): Promise<[ConfigSpec, ConfigData][]> { | ||
const data = await cfg.getData(); | ||
if (!data) { | ||
return []; | ||
} | ||
const arr = Array.isArray(data) ? data : [data]; | ||
return arr.map((d, i) => [{ | ||
priority: cfg.priority + i, | ||
source: cfg.source, | ||
...(d.__ID__ ? { detail: d.__ID__?.toString() } : {}) | ||
}, d]); | ||
} | ||
#storage: Record<string, unknown> = {}; // Lowered, and flattened | ||
#profiles: string[] = ['application', GlobalEnv.envName, ...Env.getList('TRV_PROFILES') ?? [], 'override']; | ||
#sources: string[] = []; | ||
#secrets: (RegExp | string)[] = [/secure(-|_|[a-z])|password|private|secret|salt|(api(-|_)?key)/i]; | ||
/** | ||
@@ -62,13 +69,20 @@ * Get a sub tree of the config, or everything if namespace is not passed | ||
const configs = await Promise.all( | ||
providers.map(async (el) => { | ||
const inst = await DependencyRegistry.getInstance<ConfigSource>(el.class, el.qualifier); | ||
return inst.getValues(this.#profiles); | ||
}) | ||
providers.map(async (el) => await DependencyRegistry.getInstance<ConfigSource>(el.class, el.qualifier)) | ||
); | ||
const sorted = ConfigurationService.getSorted(configs.flat(), this.#profiles); | ||
const parser = await DependencyRegistry.getInstance(ParserManager); | ||
this.#sources = sorted.map(x => `${x.profile}.${x.priority} - ${x.source}`); | ||
const specPairs = await Promise.all([ | ||
new FileConfigSource(parser, 'application', 100), | ||
new FileConfigSource(parser, GlobalEnv.envName, 200), | ||
...(Env.getList('TRV_PROFILES') ?? []).map((p, i) => new FileConfigSource(parser, p, 300 + i * 10)), | ||
...configs, | ||
new OverrideConfigSource() | ||
].map(src => this.#toSpecPairs(src))); | ||
for (const { config: element } of sorted) { | ||
const specs = specPairs.flat().sort(([a], [b]) => a.priority - b.priority); | ||
this.#specs = specs.map(([v]) => v); | ||
for (const [, element] of specs) { | ||
DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(element), 'coerce'); | ||
@@ -94,3 +108,3 @@ } | ||
*/ | ||
async exportActive(): Promise<{ sources: string[], active: ConfigData }> { | ||
async exportActive(): Promise<{ sources: ConfigSpec[], active: ConfigData }> { | ||
const configTargets = await DependencyRegistry.getCandidateTypes(ConfigTarget); | ||
@@ -113,3 +127,3 @@ const configs = await Promise.all( | ||
} | ||
return { sources: this.#sources, active: out }; | ||
return { sources: this.#specs, active: out }; | ||
} | ||
@@ -116,0 +130,0 @@ |
@@ -1,4 +0,5 @@ | ||
import { Env, GlobalEnv } from '@travetto/base'; | ||
import { Env } from '@travetto/base'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
import { ConfigData } from '../parser/types'; | ||
@@ -10,3 +11,3 @@ /** | ||
priority: number; | ||
name = 'env'; | ||
source: string; | ||
#envKey: string; | ||
@@ -17,13 +18,13 @@ | ||
this.priority = priority; | ||
this.source = `env://${this.#envKey}`; | ||
} | ||
getValues(profiles: string[]): ConfigValue[] { | ||
getData(): ConfigData | undefined { | ||
try { | ||
const data = JSON.parse(Env.get(this.#envKey, '{}')); | ||
return [{ profile: GlobalEnv.envName, config: data, source: `${this.name}://${this.#envKey}`, priority: this.priority }]; | ||
return data; | ||
} catch (e) { | ||
console.error(`env.${this.#envKey} is an invalid format`, { text: Env.get(this.#envKey) }); | ||
return []; | ||
} | ||
} | ||
} |
@@ -1,8 +0,7 @@ | ||
import { path } from '@travetto/manifest'; | ||
import { FileQueryProvider } from '@travetto/base'; | ||
import { DependencyRegistry, InjectableFactory } from '@travetto/di'; | ||
import { RootIndex, path } from '@travetto/manifest'; | ||
import { ConfigParserTarget } from '../internal/types'; | ||
import { ConfigParser } from '../parser/types'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
import { ParserManager } from '../parser/parser'; | ||
import { ConfigData } from '../parser/types'; | ||
@@ -14,47 +13,33 @@ /** | ||
@InjectableFactory() | ||
static getInstance(): ConfigSource { | ||
return new FileConfigSource(); | ||
} | ||
priority = 10; | ||
source: string; | ||
profile: string; | ||
parser: ParserManager; | ||
depth = 1; | ||
extMatch: RegExp; | ||
parsers: Record<string, ConfigParser>; | ||
priority = 1; | ||
constructor(paths: string[] = []) { | ||
constructor(parser: ParserManager, profile: string, priority: number, paths: string[] = []) { | ||
super({ includeCommon: true, paths }); | ||
this.priority = priority; | ||
this.profile = profile; | ||
this.parser = parser; | ||
this.source = `file://${profile}`; | ||
} | ||
async postConstruct(): Promise<void> { | ||
const parserClasses = await DependencyRegistry.getCandidateTypes(ConfigParserTarget); | ||
const parsers = await Promise.all(parserClasses.map(x => DependencyRegistry.getInstance<ConfigParser>(x.class, x.qualifier))); | ||
// Register parsers | ||
this.parsers = Object.fromEntries(parsers.flatMap(p => p.ext.map(e => [e, p]))); | ||
this.extMatch = parsers.length ? new RegExp(`(${Object.keys(this.parsers).join('|').replaceAll('.', '[.]')})`) : /^$/; | ||
} | ||
async getValues(profiles: string[]): Promise<ConfigValue[]> { | ||
const out: ConfigValue[] = []; | ||
for await (const file of this.query(f => this.extMatch.test(f))) { | ||
async getData(): Promise<ConfigData[]> { | ||
const out: { file: string, data: ConfigData }[] = []; | ||
for await (const file of this.query(f => this.parser.matches(f))) { | ||
const ext = path.extname(file); | ||
const profile = path.basename(file, ext); | ||
if (!profiles.includes(profile) || !this.parsers[ext]) { | ||
continue; | ||
const base = path.basename(file, ext); | ||
if (base === this.profile && !file.includes('/')) { // Ensures no nesting | ||
for (const resolved of await this.resolveAll(file)) { | ||
out.push({ file: resolved, data: await this.parser.parse(resolved) }); | ||
} | ||
} | ||
const content = await this.read(file); | ||
const desc = await this.describe(file); | ||
out.push({ | ||
profile, | ||
config: await this.parsers[ext].parse(content), | ||
source: `file://${desc.path}`, | ||
priority: this.priority | ||
}); | ||
} | ||
// Ensure more specific files are processed later | ||
return out.sort((a, b) => (a.source.length - b.source.length) || a.source.localeCompare(b.source)); | ||
return out | ||
.sort((a, b) => (a.file.length - b.file.length) || a.file.localeCompare(b.file)) | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
.map(a => ({ ...a.data, __ID__: a.file.replace(`${RootIndex.manifest.workspacePath}/`, '') })); | ||
} | ||
} |
import { ConfigData } from '../parser/types'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
@@ -8,20 +8,15 @@ /** | ||
export class MemoryConfigSource implements ConfigSource { | ||
priority = 1; | ||
data: Record<string, ConfigData>; | ||
name = 'memory'; | ||
priority: number; | ||
data: ConfigData; | ||
source: string; | ||
constructor(data: Record<string, ConfigData>, priority: number = 1) { | ||
constructor(key: string, data: ConfigData, priority: number = 500) { | ||
this.data = data; | ||
this.priority = priority; | ||
this.source = `memory://${key}`; | ||
} | ||
getValues(profiles: string[]): ConfigValue[] { | ||
const out: ConfigValue[] = []; | ||
for (const profile of profiles) { | ||
if (this.data[profile]) { | ||
out.push({ profile, config: this.data[profile], source: `${this.name}://${profile}`, priority: this.priority }); | ||
} | ||
} | ||
return out; | ||
getData(): ConfigData { | ||
return this.data; | ||
} | ||
} |
@@ -1,2 +0,1 @@ | ||
import { Injectable } from '@travetto/di'; | ||
import { SchemaRegistry } from '@travetto/schema'; | ||
@@ -6,3 +5,3 @@ | ||
import { ConfigData } from '../parser/types'; | ||
import { ConfigSource, ConfigValue } from './types'; | ||
import { ConfigSource } from './types'; | ||
@@ -13,6 +12,5 @@ /** | ||
*/ | ||
@Injectable() | ||
export class OverrideConfigSource implements ConfigSource { | ||
priority = 3; | ||
name = 'override'; | ||
priority = 999; | ||
source = 'memory://override'; | ||
@@ -33,10 +31,5 @@ #build(): ConfigData { | ||
getValues(profiles: string[]): [ConfigValue] { | ||
return [{ | ||
config: this.#build(), | ||
profile: 'override', | ||
source: 'memory://override', | ||
priority: this.priority | ||
}]; | ||
getData(): ConfigData { | ||
return this.#build(); | ||
} | ||
} |
import { ConfigData } from '../parser/types'; | ||
export type ConfigValue = { config: ConfigData, source: string, profile: string, priority: number }; | ||
/** | ||
@@ -10,3 +8,4 @@ * @concrete ../internal/types:ConfigSourceTarget | ||
priority: number; | ||
getValues(profiles: string[]): Promise<ConfigValue[]> | ConfigValue[]; | ||
} | ||
source: string; | ||
getData(): Promise<ConfigData[] | ConfigData | undefined> | ConfigData[] | ConfigData | undefined; | ||
} |
27818
17
400
249
Updated@travetto/di@^3.4.0-rc.4
Updated@travetto/schema@^3.4.0-rc.4
Updated@travetto/yaml@^3.4.0-rc.4