@travetto/config 3.4.0-rc.4 → 3.4.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -18
- package/package.json +4 -4
- package/src/parser/parser.ts +6 -1
- package/src/service.ts +16 -28
- package/src/source/env.ts +5 -8
- package/src/source/file.ts +38 -28
- package/src/source/memory.ts +5 -9
- package/src/source/override.ts +3 -6
- package/src/source/types.ts +6 -3
package/README.md
CHANGED
|
@@ -23,9 +23,14 @@ Config loading follows a defined resolution path, below is the order in increasi
|
|
|
23
23
|
1. `resources/application.<ext>` - Priority `100` - Load the default `application.<ext>` if available.
|
|
24
24
|
1. `resources/{env}.<ext>` - Priority `200` - Load environment specific profile configurations as defined by the values of `process.env.TRV_ENV`.
|
|
25
25
|
1. `resources/*.<ext>` - Priority `300` - Load profile specific configurations as defined by the values in `process.env.TRV_PROFILES`
|
|
26
|
+
1. [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L31) [ConfigSource](https://github.com/travetto/travetto/tree/main/module/config/src/source/types.ts#L11) - Priority `???` - These are custom config sources provided by the module, and are able to define their own priorities
|
|
27
|
+
1. [OverrideConfigSource](https://github.com/travetto/travetto/tree/main/module/config/src/source/override.ts#L11) - Priority `999` - This is for [EnvVar](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L34) overrides, and is at the top priority for all built-in config sources.
|
|
26
28
|
By default all configuration data is inert, and will only be applied when constructing an instance of a configuration class.
|
|
27
29
|
|
|
28
|
-
**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.
|
|
30
|
+
**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 priority is:
|
|
31
|
+
1. Mono-repo root
|
|
32
|
+
1. Module root
|
|
33
|
+
1. Folders for `TRV_RESOURCES`, in order
|
|
29
34
|
|
|
30
35
|
### A Complete Example
|
|
31
36
|
A more complete example setup would look like:
|
|
@@ -91,24 +96,20 @@ The framework provides two simple base classes that assist with existing pattern
|
|
|
91
96
|
**Code: Memory Provider**
|
|
92
97
|
```typescript
|
|
93
98
|
import { ConfigData } from '../parser/types';
|
|
94
|
-
import { ConfigSource } from './types';
|
|
99
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
95
100
|
|
|
96
101
|
/**
|
|
97
102
|
* Meant to be instantiated and provided as a unique config source
|
|
98
103
|
*/
|
|
99
104
|
export class MemoryConfigSource implements ConfigSource {
|
|
100
|
-
|
|
101
|
-
data: ConfigData;
|
|
102
|
-
source: string;
|
|
105
|
+
#spec: ConfigSpec;
|
|
103
106
|
|
|
104
107
|
constructor(key: string, data: ConfigData, priority: number = 500) {
|
|
105
|
-
this
|
|
106
|
-
this.priority = priority;
|
|
107
|
-
this.source = `memory://${key}`;
|
|
108
|
+
this.#spec = { data, priority, source: `memory://${key}` };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
return this
|
|
111
|
+
get(): ConfigSpec {
|
|
112
|
+
return this.#spec;
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
```
|
|
@@ -117,27 +118,24 @@ export class MemoryConfigSource implements ConfigSource {
|
|
|
117
118
|
```typescript
|
|
118
119
|
import { Env } from '@travetto/base';
|
|
119
120
|
|
|
120
|
-
import { ConfigSource } from './types';
|
|
121
|
-
import { ConfigData } from '../parser/types';
|
|
121
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Represents the environment mapped data as a JSON blob
|
|
125
125
|
*/
|
|
126
126
|
export class EnvConfigSource implements ConfigSource {
|
|
127
|
-
priority: number;
|
|
128
|
-
source: string;
|
|
129
127
|
#envKey: string;
|
|
128
|
+
#priority: number;
|
|
130
129
|
|
|
131
130
|
constructor(key: string, priority: number) {
|
|
132
131
|
this.#envKey = key;
|
|
133
|
-
this
|
|
134
|
-
this.source = `env://${this.#envKey}`;
|
|
132
|
+
this.#priority = priority;
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
get(): ConfigSpec | undefined {
|
|
138
136
|
try {
|
|
139
137
|
const data = JSON.parse(Env.get(this.#envKey, '{}'));
|
|
140
|
-
return data;
|
|
138
|
+
return { data, priority: this.#priority, source: `env://${this.#envKey}` };
|
|
141
139
|
} catch (e) {
|
|
142
140
|
console.error(`env.${this.#envKey} is an invalid format`, { text: Env.get(this.#envKey) });
|
|
143
141
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/config",
|
|
3
|
-
"version": "3.4.0-rc.
|
|
3
|
+
"version": "3.4.0-rc.6",
|
|
4
4
|
"description": "Configuration support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yaml",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"directory": "module/config"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/di": "^3.4.0-rc.
|
|
30
|
-
"@travetto/schema": "^3.4.0-rc.
|
|
31
|
-
"@travetto/yaml": "^3.4.0-rc.
|
|
29
|
+
"@travetto/di": "^3.4.0-rc.6",
|
|
30
|
+
"@travetto/schema": "^3.4.0-rc.6",
|
|
31
|
+
"@travetto/yaml": "^3.4.0-rc.6"
|
|
32
32
|
},
|
|
33
33
|
"travetto": {
|
|
34
34
|
"displayName": "Configuration"
|
package/src/parser/parser.ts
CHANGED
|
@@ -19,10 +19,12 @@ export class ParserManager {
|
|
|
19
19
|
|
|
20
20
|
// Register parsers
|
|
21
21
|
this.#parsers = Object.fromEntries(parsers.flatMap(p => p.ext.map(e => [e, p])));
|
|
22
|
-
|
|
23
22
|
this.#extMatch = parsers.length ? new RegExp(`(${Object.keys(this.#parsers).join('|').replaceAll('.', '[.]')})`) : /^$/;
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Attempt ot parse a file, based on file's extension
|
|
27
|
+
*/
|
|
26
28
|
async parse(file: string): Promise<ConfigData> {
|
|
27
29
|
const ext = path.extname(file);
|
|
28
30
|
if (!this.#parsers[ext]) {
|
|
@@ -31,6 +33,9 @@ export class ParserManager {
|
|
|
31
33
|
return fs.readFile(file, 'utf8').then(content => this.#parsers[ext].parse(content));
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Determine if file matches
|
|
38
|
+
*/
|
|
34
39
|
matches(file: string): boolean {
|
|
35
40
|
return this.#extMatch.test(file);
|
|
36
41
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import util from 'util';
|
|
2
2
|
|
|
3
|
-
import { AppError, Class, ClassInstance, GlobalEnv, DataUtil
|
|
3
|
+
import { AppError, Class, ClassInstance, GlobalEnv, DataUtil } from '@travetto/base';
|
|
4
4
|
import { DependencyRegistry, Injectable } from '@travetto/di';
|
|
5
5
|
import { RootIndex } from '@travetto/manifest';
|
|
6
6
|
import { BindUtil, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
@@ -8,11 +8,11 @@ import { BindUtil, SchemaRegistry, SchemaValidator, ValidationResultError } from
|
|
|
8
8
|
import { ConfigSourceTarget, ConfigTarget } from './internal/types';
|
|
9
9
|
import { ParserManager } from './parser/parser';
|
|
10
10
|
import { ConfigData } from './parser/types';
|
|
11
|
-
import { ConfigSource } from './source/types';
|
|
11
|
+
import { ConfigSource, ConfigSpec } from './source/types';
|
|
12
12
|
import { FileConfigSource } from './source/file';
|
|
13
13
|
import { OverrideConfigSource } from './source/override';
|
|
14
14
|
|
|
15
|
-
type
|
|
15
|
+
type ConfigSpecSimple = Omit<ConfigSpec, 'data'>;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Manager for application configuration
|
|
@@ -21,22 +21,9 @@ type ConfigSpec = { source: string, priority: number, detail?: string };
|
|
|
21
21
|
export class ConfigurationService {
|
|
22
22
|
|
|
23
23
|
#storage: Record<string, unknown> = {}; // Lowered, and flattened
|
|
24
|
-
#specs:
|
|
24
|
+
#specs: ConfigSpecSimple[] = [];
|
|
25
25
|
#secrets: (RegExp | string)[] = [/secure(-|_|[a-z])|password|private|secret|salt|(api(-|_)?key)/i];
|
|
26
26
|
|
|
27
|
-
async #toSpecPairs(cfg: ConfigSource): Promise<[ConfigSpec, ConfigData][]> {
|
|
28
|
-
const data = await cfg.getData();
|
|
29
|
-
if (!data) {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
const arr = Array.isArray(data) ? data : [data];
|
|
33
|
-
return arr.map((d, i) => [{
|
|
34
|
-
priority: cfg.priority + i,
|
|
35
|
-
source: cfg.source,
|
|
36
|
-
...(d.__ID__ ? { detail: d.__ID__?.toString() } : {})
|
|
37
|
-
}, d]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
27
|
/**
|
|
41
28
|
* Get a sub tree of the config, or everything if namespace is not passed
|
|
42
29
|
* @param ns The namespace of the config to search for, can be dotted for accessing sub namespaces
|
|
@@ -69,22 +56,23 @@ export class ConfigurationService {
|
|
|
69
56
|
|
|
70
57
|
const parser = await DependencyRegistry.getInstance(ParserManager);
|
|
71
58
|
|
|
72
|
-
const
|
|
73
|
-
new FileConfigSource(parser
|
|
74
|
-
new FileConfigSource(parser, GlobalEnv.envName, 200),
|
|
75
|
-
...(Env.getList('TRV_PROFILES') ?? []).map((p, i) => new FileConfigSource(parser, p, 300 + i * 10)),
|
|
59
|
+
const possible = await Promise.all([
|
|
60
|
+
new FileConfigSource(parser),
|
|
76
61
|
...configs,
|
|
77
62
|
new OverrideConfigSource()
|
|
78
|
-
].map(src =>
|
|
63
|
+
].map(src => src.get()));
|
|
79
64
|
|
|
80
|
-
const specs =
|
|
65
|
+
const specs = possible
|
|
66
|
+
.flat()
|
|
67
|
+
.filter((x): x is Exclude<typeof x, undefined> => !!x)
|
|
68
|
+
.sort((a, b) => a.priority - b.priority);
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const [, element] of specs) {
|
|
85
|
-
DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(element), 'coerce');
|
|
70
|
+
for (const spec of specs) {
|
|
71
|
+
DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(spec.data), 'coerce');
|
|
86
72
|
}
|
|
87
73
|
|
|
74
|
+
this.#specs = specs.map(({ data, ...v }) => v);
|
|
75
|
+
|
|
88
76
|
// Initialize Secrets
|
|
89
77
|
const userSpecified = (this.#get('config')?.secrets ?? []);
|
|
90
78
|
for (const el of Array.isArray(userSpecified) ? userSpecified : [userSpecified]) {
|
|
@@ -102,7 +90,7 @@ export class ConfigurationService {
|
|
|
102
90
|
* Export all active configuration, useful for displaying active state
|
|
103
91
|
* - Will not show fields marked as secret
|
|
104
92
|
*/
|
|
105
|
-
async exportActive(): Promise<{ sources:
|
|
93
|
+
async exportActive(): Promise<{ sources: ConfigSpecSimple[], active: ConfigData }> {
|
|
106
94
|
const configTargets = await DependencyRegistry.getCandidateTypes(ConfigTarget);
|
|
107
95
|
const configs = await Promise.all(
|
|
108
96
|
configTargets
|
package/src/source/env.ts
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { Env } from '@travetto/base';
|
|
2
2
|
|
|
3
|
-
import { ConfigSource } from './types';
|
|
4
|
-
import { ConfigData } from '../parser/types';
|
|
3
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Represents the environment mapped data as a JSON blob
|
|
8
7
|
*/
|
|
9
8
|
export class EnvConfigSource implements ConfigSource {
|
|
10
|
-
priority: number;
|
|
11
|
-
source: string;
|
|
12
9
|
#envKey: string;
|
|
10
|
+
#priority: number;
|
|
13
11
|
|
|
14
12
|
constructor(key: string, priority: number) {
|
|
15
13
|
this.#envKey = key;
|
|
16
|
-
this
|
|
17
|
-
this.source = `env://${this.#envKey}`;
|
|
14
|
+
this.#priority = priority;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
get(): ConfigSpec | undefined {
|
|
21
18
|
try {
|
|
22
19
|
const data = JSON.parse(Env.get(this.#envKey, '{}'));
|
|
23
|
-
return data;
|
|
20
|
+
return { data, priority: this.#priority, source: `env://${this.#envKey}` };
|
|
24
21
|
} catch (e) {
|
|
25
22
|
console.error(`env.${this.#envKey} is an invalid format`, { text: Env.get(this.#envKey) });
|
|
26
23
|
}
|
package/src/source/file.ts
CHANGED
|
@@ -1,44 +1,54 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
import { Env, FileResourceProvider, GlobalEnv } from '@travetto/base';
|
|
2
4
|
import { RootIndex, path } from '@travetto/manifest';
|
|
3
5
|
|
|
4
|
-
import { ConfigSource } from './types';
|
|
6
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
5
7
|
import { ParserManager } from '../parser/parser';
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
type Profile = [string, number] | readonly [string, number];
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* File-base config source, builds on common file resource provider
|
|
10
13
|
*/
|
|
11
|
-
export class FileConfigSource
|
|
14
|
+
export class FileConfigSource implements ConfigSource {
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parser: ParserManager;
|
|
16
|
+
#profiles: Profile[];
|
|
17
|
+
#searchPaths: string[];
|
|
18
|
+
#parser: ParserManager;
|
|
17
19
|
|
|
18
|
-
constructor(parser: ParserManager,
|
|
19
|
-
|
|
20
|
-
this
|
|
21
|
-
this
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
constructor(parser: ParserManager, paths?: string[], profiles?: Profile[]) {
|
|
21
|
+
this.#parser = parser;
|
|
22
|
+
this.#searchPaths = FileResourceProvider.resolveSearchPaths({ includeCommon: true, paths }).reverse();
|
|
23
|
+
this.#profiles = profiles ?? [
|
|
24
|
+
['application', 100],
|
|
25
|
+
[GlobalEnv.envName, 200],
|
|
26
|
+
...(Env.getList('TRV_PROFILES') ?? [])
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
28
|
+
.map((p, i) => [p, 300 + i * 10] as [string, number])
|
|
29
|
+
];
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
async
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
async get(): Promise<ConfigSpec[]> {
|
|
33
|
+
const cache: Record<string, Promise<string[]>> = {};
|
|
34
|
+
const configs: Promise<ConfigSpec>[] = [];
|
|
35
|
+
for (const [profile, priority] of this.#profiles) {
|
|
36
|
+
let i = 0;
|
|
37
|
+
for (const folder of this.#searchPaths) {
|
|
38
|
+
const files = await (cache[folder] ??= fs.readdir(folder).catch(() => []));
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (this.#parser.matches(file) && path.basename(file, path.extname(file)) === profile) {
|
|
41
|
+
const full = path.resolve(folder, file);
|
|
42
|
+
configs.push(this.#parser.parse(full).then(data => ({
|
|
43
|
+
data,
|
|
44
|
+
priority: priority + i++,
|
|
45
|
+
source: `file://${profile}`,
|
|
46
|
+
detail: full.replace(`${RootIndex.manifest.workspacePath}/`, '')
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
|
-
|
|
38
|
-
// Ensure more specific files are processed later
|
|
39
|
-
return out
|
|
40
|
-
.sort((a, b) => (a.file.length - b.file.length) || a.file.localeCompare(b.file))
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
42
|
-
.map(a => ({ ...a.data, __ID__: a.file.replace(`${RootIndex.manifest.workspacePath}/`, '') }));
|
|
52
|
+
return Promise.all(configs);
|
|
43
53
|
}
|
|
44
54
|
}
|
package/src/source/memory.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { ConfigData } from '../parser/types';
|
|
2
|
-
import { ConfigSource } from './types';
|
|
2
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Meant to be instantiated and provided as a unique config source
|
|
6
6
|
*/
|
|
7
7
|
export class MemoryConfigSource implements ConfigSource {
|
|
8
|
-
|
|
9
|
-
data: ConfigData;
|
|
10
|
-
source: string;
|
|
8
|
+
#spec: ConfigSpec;
|
|
11
9
|
|
|
12
10
|
constructor(key: string, data: ConfigData, priority: number = 500) {
|
|
13
|
-
this
|
|
14
|
-
this.priority = priority;
|
|
15
|
-
this.source = `memory://${key}`;
|
|
11
|
+
this.#spec = { data, priority, source: `memory://${key}` };
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
return this
|
|
14
|
+
get(): ConfigSpec {
|
|
15
|
+
return this.#spec;
|
|
20
16
|
}
|
|
21
17
|
}
|
package/src/source/override.ts
CHANGED
|
@@ -2,16 +2,13 @@ import { SchemaRegistry } from '@travetto/schema';
|
|
|
2
2
|
|
|
3
3
|
import { ConfigOverrides, CONFIG_OVERRIDES } from '../internal/types';
|
|
4
4
|
import { ConfigData } from '../parser/types';
|
|
5
|
-
import { ConfigSource } from './types';
|
|
5
|
+
import { ConfigSource, ConfigSpec } from './types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Overridable config source, provides ability to override field level values, currently used by
|
|
9
9
|
* - @EnvVar as a means to allow environment specific overrides
|
|
10
10
|
*/
|
|
11
11
|
export class OverrideConfigSource implements ConfigSource {
|
|
12
|
-
priority = 999;
|
|
13
|
-
source = 'memory://override';
|
|
14
|
-
|
|
15
12
|
#build(): ConfigData {
|
|
16
13
|
const out: ConfigData = {};
|
|
17
14
|
for (const cls of SchemaRegistry.getClasses()) {
|
|
@@ -26,7 +23,7 @@ export class OverrideConfigSource implements ConfigSource {
|
|
|
26
23
|
return out;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
return this.#build();
|
|
26
|
+
get(): ConfigSpec {
|
|
27
|
+
return { data: this.#build(), priority: 999, source: 'memory://override' };
|
|
31
28
|
}
|
|
32
29
|
}
|
package/src/source/types.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ConfigData } from '../parser/types';
|
|
2
2
|
|
|
3
|
+
type OrProm<T> = T | Promise<T>;
|
|
4
|
+
type OneOf<T> = T[] | T | undefined;
|
|
5
|
+
|
|
6
|
+
export type ConfigSpec = { data: ConfigData, priority: number, source: string, detail?: string };
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* @concrete ../internal/types:ConfigSourceTarget
|
|
5
10
|
*/
|
|
6
11
|
export interface ConfigSource {
|
|
7
|
-
|
|
8
|
-
source: string;
|
|
9
|
-
getData(): Promise<ConfigData[] | ConfigData | undefined> | ConfigData[] | ConfigData | undefined;
|
|
12
|
+
get(): OrProm<OneOf<ConfigSpec>>;
|
|
10
13
|
}
|