@travetto/config 7.0.0-rc.0 → 7.0.0-rc.2
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 +10 -10
- package/package.json +3 -3
- package/src/decorator.ts +6 -6
- package/src/parser/parser.ts +1 -1
- package/src/service.ts +33 -33
- package/src/source/env.ts +2 -2
- package/src/source/file.ts +5 -5
- package/src/source/memory.ts +5 -5
- package/src/source/override.ts +5 -5
- package/src/source/types.ts +2 -2
- package/src/util.ts +4 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @travetto/config
|
|
|
13
13
|
yarn add @travetto/config
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The config module provides support for loading application config on startup. Configuration values support the common [YAML](https://en.wikipedia.org/wiki/YAML) constructs as defined in [yaml](https://github.com/eemeli/yaml). Additionally, the configuration is built upon the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") module, to enforce type correctness, and allow for validation of configuration as an entrypoint into the application. Given that all [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) classes are [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19)-based classes, all the standard [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19) and [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#
|
|
16
|
+
The config module provides support for loading application config on startup. Configuration values support the common [YAML](https://en.wikipedia.org/wiki/YAML) constructs as defined in [yaml](https://github.com/eemeli/yaml). Additionally, the configuration is built upon the [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") module, to enforce type correctness, and allow for validation of configuration as an entrypoint into the application. Given that all [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) classes are [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19)-based classes, all the standard [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19) and [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L23) functionality applies.
|
|
17
17
|
|
|
18
18
|
## Resolution
|
|
19
19
|
The configuration information is comprised of:
|
|
@@ -104,27 +104,27 @@ The framework provides two simple base classes that assist with existing pattern
|
|
|
104
104
|
**Code: Memory Provider**
|
|
105
105
|
```typescript
|
|
106
106
|
import { ConfigData } from '../parser/types.ts';
|
|
107
|
-
import { ConfigSource,
|
|
107
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Meant to be instantiated and provided as a unique config source
|
|
111
111
|
*/
|
|
112
112
|
export class MemoryConfigSource implements ConfigSource {
|
|
113
|
-
#
|
|
113
|
+
#payload: ConfigPayload;
|
|
114
114
|
|
|
115
115
|
constructor(key: string, data: ConfigData, priority: number = 500) {
|
|
116
|
-
this.#
|
|
116
|
+
this.#payload = { data, priority, source: `memory://${key}` };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
get():
|
|
120
|
-
return this.#
|
|
119
|
+
get(): ConfigPayload {
|
|
120
|
+
return this.#payload;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
**Code: Environment JSON Provider**
|
|
126
126
|
```typescript
|
|
127
|
-
import { ConfigSource,
|
|
127
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Represents the environment mapped data as a JSON blob
|
|
@@ -138,7 +138,7 @@ export class EnvConfigSource implements ConfigSource {
|
|
|
138
138
|
this.#priority = priority;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
get():
|
|
141
|
+
get(): ConfigPayload | undefined {
|
|
142
142
|
try {
|
|
143
143
|
const data = JSON.parse(process.env[this.#envKey] || '{}');
|
|
144
144
|
return { data, priority: this.#priority, source: `env://${this.#envKey}` };
|
|
@@ -154,12 +154,12 @@ In addition to files and environment variables, configuration sources can also b
|
|
|
154
154
|
|
|
155
155
|
**Code: Custom Configuration Source**
|
|
156
156
|
```typescript
|
|
157
|
-
import { ConfigSource,
|
|
157
|
+
import { ConfigSource, ConfigPayload } from '@travetto/config';
|
|
158
158
|
import { Injectable } from '@travetto/di';
|
|
159
159
|
|
|
160
160
|
@Injectable()
|
|
161
161
|
export class CustomConfigSource implements ConfigSource {
|
|
162
|
-
async get(): Promise<
|
|
162
|
+
async get(): Promise<ConfigPayload> {
|
|
163
163
|
return {
|
|
164
164
|
data: { user: { name: 'bob' } },
|
|
165
165
|
source: 'custom://override',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/config",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Configuration support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yaml",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"directory": "module/config"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/di": "^7.0.0-rc.
|
|
30
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
29
|
+
"@travetto/di": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/schema": "^7.0.0-rc.2",
|
|
31
31
|
"yaml": "^2.8.1"
|
|
32
32
|
},
|
|
33
33
|
"travetto": {
|
package/src/decorator.ts
CHANGED
|
@@ -10,21 +10,21 @@ import { ConfigOverrideUtil } from './util.ts';
|
|
|
10
10
|
* @augments `@travetto/schema:Schema`
|
|
11
11
|
* @kind decorator
|
|
12
12
|
*/
|
|
13
|
-
export function Config(
|
|
13
|
+
export function Config(namespace: string) {
|
|
14
14
|
return <T extends Class>(cls: T): T => {
|
|
15
15
|
// Declare as part of global config
|
|
16
16
|
SchemaRegistryIndex.getForRegister(cls).register({ interfaces: [ConfigBaseType] });
|
|
17
17
|
|
|
18
|
-
ConfigOverrideUtil.setOverrideConfig(cls,
|
|
18
|
+
ConfigOverrideUtil.setOverrideConfig(cls, namespace);
|
|
19
19
|
|
|
20
20
|
DependencyRegistryIndex.getForRegister(cls).registerClass();
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const handle: Function = cls.prototype.postConstruct;
|
|
23
23
|
cls.prototype.postConstruct = async function (): Promise<void> {
|
|
24
24
|
// Apply config
|
|
25
|
-
const
|
|
26
|
-
await
|
|
27
|
-
await
|
|
25
|
+
const config = await DependencyRegistryIndex.getInstance(ConfigurationService);
|
|
26
|
+
await config.bindTo(cls, this, namespace);
|
|
27
|
+
await handle?.call(this);
|
|
28
28
|
};
|
|
29
29
|
return cls;
|
|
30
30
|
};
|
package/src/parser/parser.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class ParserManager {
|
|
|
16
16
|
const parsers = await DependencyRegistryIndex.getInstances(toConcrete<ConfigParser>());
|
|
17
17
|
|
|
18
18
|
// Register parsers
|
|
19
|
-
this.#parsers = Object.fromEntries(parsers.flatMap(
|
|
19
|
+
this.#parsers = Object.fromEntries(parsers.flatMap(parser => parser.ext.map(ext => [ext, parser])));
|
|
20
20
|
this.#extMatch = parsers.length ? new RegExp(`(${Object.keys(this.#parsers).join('|').replaceAll('.', '[.]')})`) : /^$/;
|
|
21
21
|
}
|
|
22
22
|
|
package/src/service.ts
CHANGED
|
@@ -6,11 +6,11 @@ import { BindUtil, DataUtil, SchemaRegistryIndex, SchemaValidator, ValidationRes
|
|
|
6
6
|
|
|
7
7
|
import { ParserManager } from './parser/parser.ts';
|
|
8
8
|
import { ConfigData } from './parser/types.ts';
|
|
9
|
-
import { ConfigSource,
|
|
9
|
+
import { ConfigSource, ConfigPayload } from './source/types.ts';
|
|
10
10
|
import { FileConfigSource } from './source/file.ts';
|
|
11
11
|
import { OverrideConfigSource } from './source/override.ts';
|
|
12
12
|
|
|
13
|
-
type ConfigSpecSimple = Omit<
|
|
13
|
+
type ConfigSpecSimple = Omit<ConfigPayload, 'data'>;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Common Type for all configuration classes
|
|
@@ -24,15 +24,15 @@ export class ConfigBaseType { }
|
|
|
24
24
|
export class ConfigurationService {
|
|
25
25
|
|
|
26
26
|
#storage: Record<string, unknown> = {}; // Lowered, and flattened
|
|
27
|
-
#
|
|
27
|
+
#payloads: ConfigSpecSimple[] = [];
|
|
28
28
|
#secrets: (RegExp | string)[] = [/secure(-|_|[a-z])|password|private|secret|salt|(\bkey|key\b)|serviceAccount|(api(-|_)?key)/i];
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Get a sub tree of the config, or everything if namespace is not passed
|
|
32
|
-
* @param
|
|
32
|
+
* @param namespace The namespace of the config to search for, can be dotted for accessing sub namespaces
|
|
33
33
|
*/
|
|
34
|
-
#get<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
35
|
-
const parts = (
|
|
34
|
+
#get<T extends Record<string, unknown> = Record<string, unknown>>(namespace?: string): T {
|
|
35
|
+
const parts = (namespace ? namespace.split('.') : []);
|
|
36
36
|
let sub: Record<string, unknown> = this.#storage;
|
|
37
37
|
|
|
38
38
|
while (parts.length && sub) {
|
|
@@ -53,7 +53,7 @@ export class ConfigurationService {
|
|
|
53
53
|
const providers = await DependencyRegistryIndex.getCandidates(toConcrete<ConfigSource>());
|
|
54
54
|
|
|
55
55
|
const configs = await Promise.all(
|
|
56
|
-
providers.map(async (
|
|
56
|
+
providers.map(async (candidate) => await DependencyRegistryIndex.getInstance<ConfigSource>(candidate.candidateType, candidate.qualifier))
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
const parser = await DependencyRegistryIndex.getInstance(ParserManager);
|
|
@@ -62,27 +62,27 @@ export class ConfigurationService {
|
|
|
62
62
|
new FileConfigSource(parser),
|
|
63
63
|
...configs,
|
|
64
64
|
new OverrideConfigSource()
|
|
65
|
-
].map(
|
|
65
|
+
].map(source => source.get()));
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const payloads = possible
|
|
68
68
|
.flat()
|
|
69
|
-
.filter(
|
|
69
|
+
.filter(data => !!data)
|
|
70
70
|
.toSorted((a, b) => a.priority - b.priority);
|
|
71
71
|
|
|
72
|
-
for (const
|
|
73
|
-
DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(
|
|
72
|
+
for (const payload of payloads) {
|
|
73
|
+
DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(payload.data), 'coerce');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
this.#
|
|
76
|
+
this.#payloads = payloads.map(({ data: _, ...payload }) => payload);
|
|
77
77
|
|
|
78
78
|
// Initialize Secrets
|
|
79
79
|
const { secrets = [] } = this.#get<{ secrets?: string | string[] }>('config') ?? {};
|
|
80
|
-
for (const
|
|
81
|
-
if (typeof
|
|
82
|
-
if (
|
|
83
|
-
this.#secrets.push(DataUtil.coerceType(
|
|
80
|
+
for (const value of [secrets].flat()) {
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
if (value.startsWith('/')) {
|
|
83
|
+
this.#secrets.push(DataUtil.coerceType(value, RegExp, true));
|
|
84
84
|
} else {
|
|
85
|
-
this.#secrets.push(DataUtil.coerceType(
|
|
85
|
+
this.#secrets.push(DataUtil.coerceType(value, String, true));
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -96,21 +96,21 @@ export class ConfigurationService {
|
|
|
96
96
|
const configTargets = await DependencyRegistryIndex.getCandidates(ConfigBaseType);
|
|
97
97
|
const configs = await Promise.all(
|
|
98
98
|
configTargets
|
|
99
|
-
.filter(
|
|
99
|
+
.filter(candidate => candidate.qualifier === getDefaultQualifier(candidate.class)) // Is self targeting?
|
|
100
100
|
.toSorted((a, b) => a.class.name.localeCompare(b.class.name))
|
|
101
|
-
.map(async
|
|
102
|
-
const inst = await DependencyRegistryIndex.getInstance(
|
|
103
|
-
return [
|
|
101
|
+
.map(async candidate => {
|
|
102
|
+
const inst = await DependencyRegistryIndex.getInstance(candidate.class, candidate.qualifier);
|
|
103
|
+
return [candidate, inst] as const;
|
|
104
104
|
})
|
|
105
105
|
);
|
|
106
106
|
const out: Record<string, ConfigData> = {};
|
|
107
|
-
for (const [
|
|
107
|
+
for (const [candidate, inst] of configs) {
|
|
108
108
|
const data = BindUtil.bindSchemaToObject<ConfigData>(
|
|
109
|
-
getClass(inst), {}, inst, { filterInput:
|
|
109
|
+
getClass(inst), {}, inst, { filterInput: field => !('secret' in field) || !field.secret, filterValue: value => value !== undefined }
|
|
110
110
|
);
|
|
111
|
-
out[
|
|
111
|
+
out[candidate.candidateType.name] = DataUtil.filterByKeys(data, this.#secrets);
|
|
112
112
|
}
|
|
113
|
-
return { sources: this.#
|
|
113
|
+
return { sources: this.#payloads, active: out };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
@@ -125,15 +125,15 @@ export class ConfigurationService {
|
|
|
125
125
|
if (validate) {
|
|
126
126
|
try {
|
|
127
127
|
await SchemaValidator.validate(cls, item);
|
|
128
|
-
} catch (
|
|
129
|
-
if (
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof ValidationResultError) {
|
|
130
|
+
const originalMessage = error.message;
|
|
131
|
+
error.message = `Failed to construct ${classId} as validation errors have occurred`;
|
|
132
|
+
error.stack = error.stack?.replace(originalMessage, error.message);
|
|
133
133
|
const imp = Runtime.getImport(cls);
|
|
134
|
-
Object.defineProperty(
|
|
134
|
+
Object.defineProperty(error, 'details', { value: { class: classId, import: imp, ...(error.details ?? {}) } });
|
|
135
135
|
}
|
|
136
|
-
throw
|
|
136
|
+
throw error;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
return item;
|
package/src/source/env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigSource,
|
|
1
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Represents the environment mapped data as a JSON blob
|
|
@@ -12,7 +12,7 @@ export class EnvConfigSource implements ConfigSource {
|
|
|
12
12
|
this.#priority = priority;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
get():
|
|
15
|
+
get(): ConfigPayload | undefined {
|
|
16
16
|
try {
|
|
17
17
|
const data = JSON.parse(process.env[this.#envKey] || '{}');
|
|
18
18
|
return { data, priority: this.#priority, source: `env://${this.#envKey}` };
|
package/src/source/file.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { Env, Runtime, RuntimeResources } from '@travetto/runtime';
|
|
5
5
|
|
|
6
|
-
import { ConfigSource,
|
|
6
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
7
7
|
import { ParserManager } from '../parser/parser.ts';
|
|
8
8
|
|
|
9
9
|
type Profile = [string, number] | readonly [string, number];
|
|
@@ -24,13 +24,13 @@ export class FileConfigSource implements ConfigSource {
|
|
|
24
24
|
['application', 100],
|
|
25
25
|
[Runtime.env!, 200],
|
|
26
26
|
...(Env.TRV_PROFILES.list ?? [])
|
|
27
|
-
.map((
|
|
28
|
-
] as const).filter(
|
|
27
|
+
.map((profile, i) => [profile, 300 + i * 10] as const)
|
|
28
|
+
] as const).filter(entry => !!entry[0]);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async get(): Promise<
|
|
31
|
+
async get(): Promise<ConfigPayload[]> {
|
|
32
32
|
const cache: Record<string, Promise<string[]>> = {};
|
|
33
|
-
const configs: Promise<
|
|
33
|
+
const configs: Promise<ConfigPayload>[] = [];
|
|
34
34
|
|
|
35
35
|
for (const [profile, priority] of this.#profiles) {
|
|
36
36
|
let i = priority;
|
package/src/source/memory.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { ConfigData } from '../parser/types.ts';
|
|
2
|
-
import { ConfigSource,
|
|
2
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
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
|
-
#
|
|
8
|
+
#payload: ConfigPayload;
|
|
9
9
|
|
|
10
10
|
constructor(key: string, data: ConfigData, priority: number = 500) {
|
|
11
|
-
this.#
|
|
11
|
+
this.#payload = { data, priority, source: `memory://${key}` };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
get():
|
|
15
|
-
return this.#
|
|
14
|
+
get(): ConfigPayload {
|
|
15
|
+
return this.#payload;
|
|
16
16
|
}
|
|
17
17
|
}
|
package/src/source/override.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigData } from '../parser/types.ts';
|
|
2
|
-
import { ConfigSource,
|
|
2
|
+
import { ConfigSource, ConfigPayload } from './types.ts';
|
|
3
3
|
import { ConfigOverrideUtil } from '../util.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -11,16 +11,16 @@ export class OverrideConfigSource implements ConfigSource {
|
|
|
11
11
|
const out: ConfigData = {};
|
|
12
12
|
for (const { namespace, fields } of ConfigOverrideUtil.getAllOverrideConfigs()) {
|
|
13
13
|
for (const [key, value] of Object.entries(fields)) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
out[`${namespace}.${key}`] =
|
|
14
|
+
const data = value();
|
|
15
|
+
if (data !== undefined && data !== '') {
|
|
16
|
+
out[`${namespace}.${key}`] = data;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
return out;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
get():
|
|
23
|
+
get(): ConfigPayload {
|
|
24
24
|
return { data: this.#build(), priority: 999, source: 'memory://override' };
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/source/types.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { ConfigData } from '../parser/types.ts';
|
|
|
3
3
|
type OrProm<T> = T | Promise<T>;
|
|
4
4
|
type OneOf<T> = T[] | T | undefined;
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type ConfigPayload = { data: ConfigData, priority: number, source: string, detail?: string };
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @concrete
|
|
10
10
|
*/
|
|
11
11
|
export interface ConfigSource {
|
|
12
|
-
get(): OrProm<OneOf<
|
|
12
|
+
get(): OrProm<OneOf<ConfigPayload>>;
|
|
13
13
|
}
|
package/src/util.ts
CHANGED
|
@@ -22,9 +22,9 @@ export class ConfigOverrideUtil {
|
|
|
22
22
|
static getAllOverrideConfigs(): Required<OverrideConfig>[] {
|
|
23
23
|
const out: Required<OverrideConfig>[] = [];
|
|
24
24
|
for (const cls of SchemaRegistryIndex.getClasses()) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
out.push(asFull(
|
|
25
|
+
const config = this.getOverrideConfig(cls);
|
|
26
|
+
if (config && config.fields && config.namespace) {
|
|
27
|
+
out.push(asFull(config));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return out;
|
|
@@ -35,7 +35,7 @@ export class ConfigOverrideUtil {
|
|
|
35
35
|
.registerMetadata<OverrideConfig>(OverrideConfigSymbol, {});
|
|
36
36
|
|
|
37
37
|
(env.fields ??= {})[field] = (): string | undefined =>
|
|
38
|
-
process.env[names.find(
|
|
38
|
+
process.env[names.find(name => !!process.env[name])!];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
static setOverrideConfig(cls: Class<Any>, namespace: string): void {
|