@travetto/config 3.0.0-rc.4 → 3.0.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/main/module/config/doc.ts and execute "npx trv doc" to rebuild -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/config/DOC.ts and execute "npx trv doc" to rebuild -->
3
3
  # Configuration
4
- ## Environment-aware config management using yaml files
4
+ ## Configuration support
5
5
 
6
6
  **Install: @travetto/config**
7
7
  ```bash
8
8
  npm install @travetto/config
9
9
  ```
10
10
 
11
- 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/travetto/travetto/tree/main/module/yaml#readme "Simple YAML support, provides only clean subset of 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
12
- entrypoint into the application. Given that all [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L9) classes are -based classes, all the standard and functionality applies.
11
+ 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/travetto/travetto/tree/main/module/yaml#readme "Simple YAML support, provides only clean subset of 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
12
+ 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#L14)-based classes, all the standard [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L14) and [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L38) functionality applies.
13
13
 
14
14
  ## Resolution
15
15
 
@@ -66,32 +66,40 @@ At runtime the resolved config would be:
66
66
 
67
67
  **Terminal: Runtime Resolution**
68
68
  ```bash
69
- $ node @travetto/boot/bin/main ./doc/resolve.ts
69
+ $ trv main support/main.resolve.ts
70
70
 
71
71
  Config {
72
- database: {
73
- host: 'localhost',
74
- port: 1234,
75
- creds: { user: 'test', password: '**********' }
72
+ sources: [
73
+ 'application.1 - file://application.yml',
74
+ 'override.3 - memory://override'
75
+ ],
76
+ active: {
77
+ DBConfig: {
78
+ host: 'localhost',
79
+ port: 2000,
80
+ creds: creds_7_45Ⲑsyn { user: 'test', password: 'test' }
81
+ }
76
82
  }
77
83
  }
84
+ 
78
85
  ```
79
86
 
80
87
  ## Secrets
81
88
  By default, when in production mode, the application startup will request redacted secrets to log out. These secrets follow a standard set of rules, but can be amended by listing regular expressions under `config.redacted`.
82
89
 
83
90
  ## Consuming
84
- The [ConfigManager](https://github.com/travetto/travetto/tree/main/module/config/src/manager.ts) service provides direct access to all of the loaded configuration. For simplicity, a decorator, [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L9) 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.
91
+ The [Configuration](https://github.com/travetto/travetto/tree/main/module/config/src/configuration.ts#L14) 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.
85
92
 
86
93
  The decorator takes in a namespace, of what part of the resolved configuration you want to bind to your class. Given the following class:
87
94
 
88
95
  **Code: Database config object**
89
96
  ```typescript
90
- import { Config } from '@travetto/config';
97
+ import { Config, EnvVar } from '@travetto/config';
91
98
 
92
99
  @Config('database')
93
100
  export class DBConfig {
94
101
  host: string;
102
+ @EnvVar('DATABASE_PORT')
95
103
  port: number;
96
104
  creds: {
97
105
  user: string;
@@ -104,25 +112,9 @@ Using the above config files, you'll notice that the port is not specified (its
104
112
 
105
113
  **Terminal: Resolved database config**
106
114
  ```bash
107
- $ node @travetto/boot/bin/main ./doc/dbconfig-run.ts
108
-
109
- {
110
- message: 'Failed to construct @trv:config/doc/dbconfig○DBConfig as validation errors have occurred',
111
- category: 'data',
112
- type: 'ValidationResultError',
113
- at: 2022-03-14T04:00:00.618Z,
114
- class: '@trv:config/doc/dbconfig○DBConfig',
115
- file: '@trv:config/doc/dbconfig.ts',
116
- errors: [
117
- {
118
- kind: 'required',
119
- value: undefined,
120
- message: 'port is required',
121
- path: 'port',
122
- type: undefined
123
- }
124
- ]
125
- }
115
+ $ trv main support/main.dbconfig-run.ts
116
+
117
+ 
126
118
  ```
127
119
 
128
120
  What you see, is that the configuration structure must be honored and the application will fail to start if the constraints do not hold true. This helps to ensure that the configuration, as input to the system, is verified and correct.
@@ -131,13 +123,20 @@ By passing in the port via the environment variable, the config will construct p
131
123
 
132
124
  **Terminal: Resolved database config**
133
125
  ```bash
134
- $ node @travetto/boot/bin/main ./doc/dbconfig-run.ts
126
+ $ trv main support/main.dbconfig-run.ts
135
127
 
136
128
  Config {
137
- database: {
138
- host: 'localhost',
139
- port: 200,
140
- creds: { user: 'test', password: 'test' }
129
+ sources: [
130
+ 'application.1 - file://application.yml',
131
+ 'override.3 - memory://override'
132
+ ],
133
+ active: {
134
+ DBConfig: {
135
+ host: 'localhost',
136
+ port: 200,
137
+ creds: creds_7_45Ⲑsyn { user: 'test', password: 'test' }
138
+ }
141
139
  }
142
140
  }
141
+ 
143
142
  ```
package/__index__.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './src/decorator';
2
+ export * from './src/configuration';
3
+ export * from './src/source/file';
4
+ export * from './src/parser/json';
5
+ export * from './src/parser/types';
6
+ export * from './src/parser/properties';
7
+ export * from './src/source/override';
8
+ export * from './src/source/memory';
9
+ export * from './src/source/types';
10
+ export * from './src/parser/yaml';
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@travetto/config",
3
- "displayName": "Configuration",
4
- "version": "3.0.0-rc.4",
5
- "description": "Environment-aware config management using yaml files",
3
+ "version": "3.0.0-rc.6",
4
+ "description": "Configuration support",
6
5
  "keywords": [
7
6
  "yaml",
8
7
  "decorators",
@@ -18,22 +17,21 @@
18
17
  "name": "Travetto Framework"
19
18
  },
20
19
  "files": [
21
- "index.ts",
22
- "src",
23
- "support"
20
+ "__index__.ts",
21
+ "src"
24
22
  ],
25
- "main": "index.ts",
23
+ "main": "__index__.ts",
26
24
  "repository": {
27
25
  "url": "https://github.com/travetto/travetto.git",
28
26
  "directory": "module/config"
29
27
  },
30
28
  "dependencies": {
31
- "@travetto/schema": "^3.0.0-rc.4",
32
- "@travetto/transformer": "^3.0.0-rc.4",
33
- "@travetto/yaml": "^3.0.0-rc.2"
29
+ "@travetto/di": "^3.0.0-rc.6",
30
+ "@travetto/schema": "^3.0.0-rc.6",
31
+ "@travetto/yaml": "^3.0.0-rc.4"
34
32
  },
35
- "devDependencies": {
36
- "@travetto/di": "^3.0.0-rc.4"
33
+ "travetto": {
34
+ "displayName": "Configuration"
37
35
  },
38
36
  "publishConfig": {
39
37
  "access": "public"
@@ -0,0 +1,119 @@
1
+ import { AppError, Class, ClassInstance, GlobalEnv, DataUtil } from '@travetto/base';
2
+ import { DependencyRegistry, Injectable } from '@travetto/di';
3
+ import { RootIndex } from '@travetto/manifest';
4
+ import { BindUtil, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
5
+
6
+ import { ConfigSourceTarget, ConfigTarget } from './internal/types';
7
+ import { ConfigData } from './parser/types';
8
+ import { ConfigSource, ConfigValue } from './source/types';
9
+
10
+ /**
11
+ * Manager for application configuration
12
+ */
13
+ @Injectable()
14
+ export class Configuration {
15
+
16
+ private static getSorted(configs: ConfigValue[], profiles: string[]): ConfigValue[] {
17
+ const order = Object.fromEntries(Object.entries(profiles).map(([k, v]) => [v, +k] as const));
18
+
19
+ return configs.sort((left, right) =>
20
+ (order[left.profile] - order[right.profile]) ||
21
+ left.priority - right.priority ||
22
+ left.source.localeCompare(right.source)
23
+ );
24
+ }
25
+
26
+ #storage: Record<string, unknown> = {}; // Lowered, and flattened
27
+ #profiles: string[] = ['application', ...GlobalEnv.profiles, 'override'];
28
+ #sources: string[] = [];
29
+
30
+ /**
31
+ * Get a sub tree of the config, or everything if namespace is not passed
32
+ * @param ns The namespace of the config to search for, can be dotted for accessing sub namespaces
33
+ */
34
+ #get(ns?: string): Record<string, unknown> {
35
+ const parts = (ns ? ns.split('.') : []);
36
+ let sub: Record<string, unknown> = this.#storage;
37
+
38
+ while (parts.length && sub) {
39
+ const next = parts.shift()!;
40
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
41
+ sub = sub[next] as Record<string, unknown>;
42
+ }
43
+
44
+ return sub;
45
+ }
46
+
47
+ /**
48
+ * Load configurations for active profiles. Load order is defined by:
49
+ * - First in order of profile names (application, ...specified, override)
50
+ * - When dealing with two profiles of the same name, they are then sorted by priority
51
+ * - If of the same priority, then alpha sort on the source
52
+ */
53
+ async postConstruct(): Promise<void> {
54
+ const providers = await DependencyRegistry.getCandidateTypes(ConfigSourceTarget);
55
+
56
+ const configs = await Promise.all(
57
+ providers.map(async (el) => {
58
+ const inst = await DependencyRegistry.getInstance<ConfigSource>(el.class, el.qualifier);
59
+ return inst.getValues(this.#profiles);
60
+ })
61
+ );
62
+
63
+ const sorted = Configuration.getSorted(configs.flat(), this.#profiles);
64
+
65
+ this.#sources = sorted.map(x => `${x.profile}.${x.priority} - ${x.source}`);
66
+
67
+ for (const { config: element } of sorted) {
68
+ DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(element), 'coerce');
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Export all active configuration, useful for displaying active state
74
+ * - Will not show fields marked as secret
75
+ */
76
+ async exportActive(): Promise<{ sources: string[], active: ConfigData }> {
77
+ const configTargets = await DependencyRegistry.getCandidateTypes(ConfigTarget);
78
+ const configs = await Promise.all(
79
+ configTargets
80
+ .filter(el => el.qualifier === DependencyRegistry.get(el.class).qualifier) // Is primary?
81
+ .sort((a, b) => a.class.name.localeCompare(b.class.name))
82
+ .map(async el => {
83
+ const inst = await DependencyRegistry.getInstance<ClassInstance>(el.class, el.qualifier);
84
+ return [el, inst] as const;
85
+ })
86
+ );
87
+ const out: Record<string, ConfigData> = {};
88
+ for (const [el, inst] of configs) {
89
+ const data = BindUtil.bindSchemaToObject<ConfigData>(
90
+ inst.constructor, {}, inst, { filterField: f => !f.secret, filterValue: v => v !== undefined }
91
+ );
92
+ out[el.class.name] = data;
93
+ }
94
+ return { sources: this.#sources, active: out };
95
+ }
96
+
97
+ /**
98
+ * Bind and validate configuration into class instance
99
+ */
100
+ async bindTo<T>(cls: Class<T>, item: T, namespace: string, validate = true): Promise<T> {
101
+ if (!SchemaRegistry.has(cls)) {
102
+ throw new AppError(`${cls.Ⲑid} is not a valid schema class, config is not supported`);
103
+ }
104
+ const out = BindUtil.bindSchemaToObject(cls, item, this.#get(namespace));
105
+ if (validate) {
106
+ try {
107
+ await SchemaValidator.validate(cls, out);
108
+ } catch (err) {
109
+ if (err instanceof ValidationResultError) {
110
+ err.message = `Failed to construct ${cls.Ⲑid} as validation errors have occurred`;
111
+ const file = RootIndex.getFunctionMetadata(cls)!.source;
112
+ err.payload = { class: cls.Ⲑid, file, ...(err.payload ?? {}) };
113
+ }
114
+ throw err;
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+ }
package/src/decorator.ts CHANGED
@@ -1,22 +1,39 @@
1
- import { Class } from '@travetto/base';
2
- import { ConfigManager } from './manager';
1
+ import { Class, ClassInstance } from '@travetto/base';
2
+ import { DependencyRegistry } from '@travetto/di';
3
+ import { SchemaRegistry } from '@travetto/schema';
4
+
5
+ import { Configuration } from './configuration';
6
+ import { ConfigTarget, ConfigOverrides, CONFIG_OVERRIDES } from './internal/types';
3
7
 
4
8
  /**
5
9
  * Indicates that the given class should be populated with the configured fields, on instantiation
6
- * @augments `@trv:schema/Schema`
7
- * @augments `@trv:di/Injectable`
10
+ * @augments `@travetto/schema:Schema`
11
+ * @augments `@travetto/di:Injectable`
8
12
  */
9
- export function Config(ns: string, params?: { internal?: boolean }) {
13
+ export function Config(ns: string) {
10
14
  return <T extends Class>(target: T): T => {
11
- const og = target.prototype.postConstruct;
15
+ const og: Function = target.prototype.postConstruct;
16
+ // Declare as part of global config
17
+ (DependencyRegistry.getOrCreatePending(target).interfaces ??= []).push(ConfigTarget);
18
+ const env = SchemaRegistry.getOrCreatePendingMetadata<ConfigOverrides>(target, CONFIG_OVERRIDES, { ns, fields: {} });
19
+ env.ns = ns;
12
20
 
13
21
  target.prototype.postConstruct = async function (): Promise<void> {
14
22
  // Apply config
15
- await ConfigManager.install(target, this, ns, params?.internal);
16
- if (og) {
17
- await og.call(this);
18
- }
23
+ const cfg = await DependencyRegistry.getInstance(Configuration);
24
+ await cfg.bindTo(target, this, ns);
25
+ await og?.call(this);
19
26
  };
20
27
  return target;
21
28
  };
29
+ }
30
+
31
+ /**
32
+ * Allows for binding specific fields to environment variables as a top-level override
33
+ */
34
+ export function EnvVar(name: string) {
35
+ return (inst: ClassInstance, prop: string): void => {
36
+ const env = SchemaRegistry.getOrCreatePendingMetadata<ConfigOverrides>(inst.constructor, CONFIG_OVERRIDES, { ns: '', fields: {} });
37
+ env.fields[prop] = (): string | undefined => process.env[name];
38
+ };
22
39
  }
@@ -0,0 +1,10 @@
1
+ export abstract class ConfigSourceTarget { }
2
+ export abstract class ConfigTarget { }
3
+ export abstract class ConfigParserTarget { }
4
+
5
+ export const CONFIG_OVERRIDES = Symbol.for('@travetto/config:field-override');
6
+
7
+ export type ConfigOverrides = {
8
+ ns: string;
9
+ fields: Record<string, () => (unknown | undefined)>;
10
+ };
@@ -0,0 +1,9 @@
1
+ import { Injectable } from '@travetto/di';
2
+
3
+ import { ConfigParser } from './types';
4
+
5
+ @Injectable()
6
+ export class JSONConfigParser implements ConfigParser {
7
+ ext = ['json'];
8
+ parse = JSON.parse.bind(JSON);
9
+ }
@@ -0,0 +1,54 @@
1
+ import { Injectable } from '@travetto/di';
2
+
3
+ import { ConfigData, ConfigParser } from './types';
4
+
5
+ const BACKSLASH = '\\'.charCodeAt(0);
6
+ const EQUALS = '='.charCodeAt(0);
7
+ const COLON = ':'.charCodeAt(0);
8
+ const HASH = '#'.charCodeAt(0);
9
+ const EXCL = '!'.charCodeAt(0);
10
+
11
+ @Injectable()
12
+ export class PropertiesConfigParser implements ConfigParser {
13
+
14
+ static parseLine(text: string): [key: string, value: string] | undefined {
15
+ if (text.charCodeAt(0) === HASH || text.charCodeAt(0) === EXCL) {
16
+ return;
17
+ }
18
+ const key: number[] = [];
19
+ let value: string | undefined;
20
+ for (let i = 0; i < text.length; i += 1) {
21
+ const ch = text.charCodeAt(i);
22
+ if (ch === EQUALS || ch === COLON) { // Break
23
+ value = text.substring(i + 1).trimStart();
24
+ break;
25
+ } else if (ch === BACKSLASH) {
26
+ key.push(text.charCodeAt(i += 1));
27
+ } else {
28
+ key.push(ch);
29
+ }
30
+ }
31
+ if (value) {
32
+ return [String.fromCharCode(...key).trimEnd(), value];
33
+ }
34
+ }
35
+
36
+ ext = ['properties'];
37
+
38
+ parse(text: string): ConfigData {
39
+ const out: ConfigData = {};
40
+ const lines = text.split(/\n/g);
41
+
42
+ for (let i = 0; i < lines.length; i++) {
43
+ let line = lines[i];
44
+ while (i < text.length && line.endsWith('\\')) {
45
+ line = `${line.replace(/\\$/, '')}${lines[i += 1].trimStart()}`;
46
+ }
47
+ const entry = PropertiesConfigParser.parseLine(line);
48
+ if (entry) {
49
+ out[entry[0]] = entry[1];
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ }
@@ -0,0 +1,9 @@
1
+ export type ConfigData = Record<string, unknown>;
2
+
3
+ /**
4
+ * @concrete ../internal/types:ConfigParserTarget
5
+ */
6
+ export interface ConfigParser {
7
+ ext: string[];
8
+ parse(input: string): Promise<ConfigData> | ConfigData;
9
+ }
@@ -0,0 +1,10 @@
1
+ import { Injectable } from '@travetto/di';
2
+ import { YamlUtil } from '@travetto/yaml';
3
+
4
+ import { ConfigParser } from './types';
5
+
6
+ @Injectable()
7
+ export class YAMLConfigParser implements ConfigParser {
8
+ ext = ['yaml', 'yml'];
9
+ parse = YamlUtil.parse;
10
+ }
@@ -0,0 +1,53 @@
1
+ import { CommonFileResourceProvider } from '@travetto/base';
2
+ import { DependencyRegistry, Injectable } from '@travetto/di';
3
+
4
+ import { ConfigParserTarget } from '../internal/types';
5
+ import { ConfigParser } from '../parser/types';
6
+ import { ConfigSource, ConfigValue } from './types';
7
+
8
+ /**
9
+ * File-base config source, builds on common file resource provider
10
+ */
11
+ @Injectable()
12
+ export class FileConfigSource extends CommonFileResourceProvider implements ConfigSource {
13
+
14
+ depth = 1;
15
+ extMatch: RegExp;
16
+ parsers: Record<string, ConfigParser>;
17
+ priority = 1;
18
+
19
+ async postConstruct(): Promise<void> {
20
+ const parserClasses = await DependencyRegistry.getCandidateTypes(ConfigParserTarget);
21
+ const parsers = await Promise.all(parserClasses.map(x => DependencyRegistry.getInstance<ConfigParser>(x.class, x.qualifier)));
22
+
23
+ // Register parsers
24
+ this.parsers = {};
25
+ for (const par of parsers) {
26
+ for (const ext of par.ext) {
27
+ this.parsers[ext] = par;
28
+ }
29
+ }
30
+
31
+ this.extMatch = parsers.length ? new RegExp(`[.](${Object.keys(this.parsers).join('|')})`) : /^$/;
32
+ }
33
+
34
+ async getValues(profiles: string[]): Promise<ConfigValue[]> {
35
+ const out: ConfigValue[] = [];
36
+
37
+ for (const file of await this.query(f => this.extMatch.test(f))) {
38
+ const ext = file.split('.')[1];
39
+ const profile = file.replace(`.${ext}`, '');
40
+ if (!profiles.includes(profile) || !this.parsers[ext]) {
41
+ continue;
42
+ }
43
+ const content = await this.read(file);
44
+ out.push({
45
+ profile,
46
+ config: await this.parsers[ext].parse(content),
47
+ source: `file://${file}`,
48
+ priority: this.priority
49
+ });
50
+ }
51
+ return out;
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ import { ConfigData } from '../parser/types';
2
+ import { ConfigSource, ConfigValue } from './types';
3
+
4
+ /**
5
+ * Meant to be instantiated and provided as a unique config source
6
+ */
7
+ export class MemoryConfigSource implements ConfigSource {
8
+ priority = 1;
9
+ data: Record<string, ConfigData>;
10
+ name = 'memory';
11
+
12
+ constructor(data: Record<string, ConfigData>, priority: number = 1) {
13
+ this.data = data;
14
+ this.priority = priority;
15
+ }
16
+
17
+ getValues(profiles: string[]): ConfigValue[] {
18
+ const out: ConfigValue[] = [];
19
+ for (const profile of profiles) {
20
+ if (this.data[profile]) {
21
+ out.push({ profile, config: this.data[profile], source: `${this.name}://${profile}`, priority: this.priority });
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ }
@@ -0,0 +1,39 @@
1
+ import { Injectable } from '@travetto/di';
2
+ import { SchemaRegistry } from '@travetto/schema';
3
+
4
+ import { ConfigOverrides, CONFIG_OVERRIDES } from '../internal/types';
5
+ import { ConfigData } from '../parser/types';
6
+ import { ConfigSource, ConfigValue } from './types';
7
+
8
+ /**
9
+ * Overridable config source, provides ability to override field level values, currently used by
10
+ * - @EnvVar as a means to allow environment specific overrides
11
+ */
12
+ @Injectable()
13
+ export class OverrideConfigSource implements ConfigSource {
14
+ priority = 3;
15
+ name = 'override';
16
+
17
+ #build(): ConfigData {
18
+ const out: ConfigData = {};
19
+ for (const cls of SchemaRegistry.getClasses()) {
20
+ const { ns, fields = {} } = SchemaRegistry.getMetadata<ConfigOverrides>(cls, CONFIG_OVERRIDES) ?? {};
21
+ for (const [key, value] of Object.entries(fields)) {
22
+ const val = value();
23
+ if (val !== undefined && val !== '') {
24
+ out[`${ns}.${key}`] = val;
25
+ }
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+
31
+ getValues(profiles: string[]): [ConfigValue] {
32
+ return [{
33
+ config: this.#build(),
34
+ profile: 'override',
35
+ source: 'memory://override',
36
+ priority: this.priority
37
+ }];
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ import { ConfigData } from '../parser/types';
2
+
3
+ export type ConfigValue = { config: ConfigData, source: string, profile: string, priority: number };
4
+
5
+ /**
6
+ * @concrete ../internal/types:ConfigSourceTarget
7
+ */
8
+ export interface ConfigSource {
9
+ priority: number;
10
+ getValues(profiles: string[]): Promise<ConfigValue[]> | ConfigValue[];
11
+ }
package/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './src/decorator';
2
- export * from './src/manager';
@@ -1,129 +0,0 @@
1
- import { Class, ResourceManager, Util } from '@travetto/base';
2
- import { YamlUtil } from '@travetto/yaml';
3
- import { BindUtil, SchemaRegistry, ViewConfig } from '@travetto/schema';
4
-
5
- /**
6
- * Simple Config Utilities
7
- */
8
- export class ConfigUtil {
9
-
10
- /**
11
- * Find the key using case insensitive search
12
- */
13
- static #getKeyName(key: string, fields: string[]): string | undefined {
14
- key = key.trim();
15
- const match = new RegExp(key, 'i');
16
- const next = fields.find(x => match.test(x));
17
- return next;
18
- }
19
-
20
- /**
21
- * Takes a env var, and produces a partial object against a schema definition. Does not support arrays, only objects.
22
- */
23
- static #expandEnvEntry(cls: Class, key: string, value: unknown): Record<string, unknown> | undefined {
24
- const parts = key.split('_');
25
-
26
- const lastPart = parts.pop()!;
27
-
28
- if (!lastPart) {
29
- return;
30
- }
31
-
32
- let cfg: ViewConfig | undefined = SchemaRegistry.getViewSchema(cls);
33
- let data: Record<string, unknown> = {};
34
- const root = data;
35
-
36
- while (parts.length) {
37
- let part = parts.shift();
38
- if (cfg) {
39
- part = this.#getKeyName(part!, cfg.fields);
40
- if (!part) {
41
- return;
42
- }
43
- const subType: Class = cfg.schema[part].type;
44
- if (SchemaRegistry.has(subType)) {
45
- cfg = SchemaRegistry.getViewSchema(subType);
46
- } else if (subType === Object) { // wildcard
47
- cfg = undefined;
48
- } else {
49
- break;
50
- }
51
- }
52
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
53
- data = ((data[part!] ??= {}) as Record<string, unknown>); // Recurse
54
- }
55
-
56
- const lastKey = (cfg ? this.#getKeyName(lastPart, cfg.fields) : undefined) ?? (/^[A-Z_0-9]+$/.test(lastPart) ? lastPart.toLowerCase() : lastPart);
57
- if (typeof value === 'string' && value.includes(',')) {
58
- value = value.trim().split(/\s*,\s*/);
59
- }
60
- data[lastKey] = value;
61
-
62
- return root;
63
- }
64
-
65
- /**
66
- * Bind the environment variables onto an object structure when they match by name.
67
- * Will split on _ to handle nesting appropriately
68
- */
69
- static getEnvOverlay(cls: Class, ns: string): Record<string, unknown> {
70
- // Handle process.env on bind as the structure we need may not
71
- // fully exist until the config has been created
72
- const nsRe = new RegExp(`^${ns.replace(/[.]/g, '_')}`, 'i'); // Check is case insensitive
73
- const data: Record<string, unknown> = {};
74
- for (const [k, v] of Object.entries(process.env)) { // Find all keys that match ns
75
- if (k.includes('_') && nsRe.test(k)) { // Require at least one level (nothing should be at the top level as all configs are namespaced)
76
- Util.deepAssign(data, this.#expandEnvEntry(cls, ns ? k.substring(ns.length + 1) : k, v), 'coerce');
77
- }
78
- }
79
- return data;
80
- }
81
-
82
- /**
83
- * Parse config file from YAML into JSON
84
- */
85
- static async getConfigFileAsData(file: string, ns: string = ''): Promise<Record<string, unknown>> {
86
- const data = await ResourceManager.read(file, 'utf8');
87
- const doc = YamlUtil.parse<Record<string, unknown>>(data);
88
- return ns ? { [ns]: doc } : doc;
89
- }
90
-
91
- /**
92
- * Looks up root object by namespace
93
- * @param src
94
- * @param ns
95
- * @returns
96
- */
97
- static lookupRoot(src: Record<string, unknown>, ns?: string, createIfMissing = false): Record<string, unknown> {
98
- const parts = (ns ? ns.split('.') : []);
99
- let sub: Record<string, unknown> = src;
100
-
101
- while (parts.length && sub) {
102
- const next = parts.shift()!;
103
- if (createIfMissing && !sub[next]) {
104
- sub[next] = {};
105
- }
106
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
107
- sub = sub[next] as Record<string, unknown>;
108
- }
109
-
110
- return sub;
111
- }
112
-
113
- /**
114
- * Sanitize payload
115
- */
116
- static sanitizeValuesByKey<T extends Record<string, unknown>>(obj: T, patterns: string[]): T {
117
- // Support custom redacted keys
118
- const regex = new RegExp(`(${patterns.filter(x => !!x).join('|')})`, 'i');
119
-
120
- const full = BindUtil.flattenPaths(obj);
121
- for (const [k, value] of Object.entries(full)) {
122
- if (typeof value === 'string' && regex.test(k)) {
123
- full[k] = '*'.repeat(10);
124
- }
125
- }
126
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
127
- return BindUtil.expandPaths(full) as T;
128
- }
129
- }
package/src/manager.ts DELETED
@@ -1,133 +0,0 @@
1
- import * as path from 'path';
2
-
3
- import { AppError, AppManifest, Class, ResourceManager, Util } from '@travetto/base';
4
- import { EnvUtil } from '@travetto/boot';
5
- import { BindUtil, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
6
-
7
- import { ConfigUtil } from './internal/util';
8
-
9
- /**
10
- * Manager for application configuration
11
- */
12
- class $ConfigManager {
13
-
14
- #initialized?: boolean = false;
15
- #storage: Record<string, unknown> = {}; // Lowered, and flattened
16
- #active: Record<string, Record<string, unknown>> = {}; // All active configs
17
- #redactedKeys = [
18
- 'passphrase.*',
19
- 'password.*',
20
- 'credential.*',
21
- '.*secret.*',
22
- '.*key',
23
- '.*token',
24
- 'pw',
25
- ];
26
-
27
- protected getStorage(): Record<string, unknown> {
28
- return this.#storage;
29
- }
30
-
31
- /**
32
- * Load all config files
33
- */
34
- async #load(): Promise<void> {
35
- const profileIndex = Object.fromEntries(Object.entries(AppManifest.env.profiles).map(([k, v]) => [v, +k] as const));
36
-
37
- const files = (await ResourceManager.findAll(/[.]ya?ml$/))
38
- .map(file => ({ file, profile: path.basename(file).replace(/[.]ya?ml$/, '') }))
39
- .filter(({ profile }) => profile in profileIndex)
40
- .sort((a, b) => profileIndex[a.profile] - profileIndex[b.profile]);
41
-
42
- if (files.length) {
43
- console.debug('Found configurations for', { files: files.map(x => x.profile) });
44
- }
45
-
46
- for (const f of files) {
47
- const data = await ConfigUtil.getConfigFileAsData(f.file);
48
- Util.deepAssign(this.#storage, BindUtil.expandPaths(data), 'coerce');
49
- }
50
- }
51
-
52
- /**
53
- * Get a sub tree of the config, or everything if namespace is not passed
54
- * @param ns The namespace of the config to search for, can be dotted for accessing sub namespaces
55
- */
56
- #get(ns?: string): Record<string, unknown> {
57
- return ConfigUtil.lookupRoot(this.#storage, ns);
58
- }
59
-
60
- /**
61
- * Order of specificity (least to most)
62
- * - Resource application.yml
63
- * - Resource {profile}.yml
64
- * - Resource {env}.yml
65
- * - Environment vars -> Overrides everything (happens at bind time)
66
- */
67
- async init(): Promise<void> {
68
- if (!this.#initialized) {
69
- this.#initialized = true;
70
- await this.#load();
71
- }
72
- }
73
-
74
- /**
75
- * Output to JSON
76
- * @param namespace If only a portion of the config should be exported
77
- * @param secure Determines if secrets should be redacted, defaults to true in prod, false otherwise
78
- */
79
- toJSON(secure: boolean = EnvUtil.isProd()): Record<string, unknown> {
80
- const copy = JSON.parse(JSON.stringify(this.#active));
81
- return secure ?
82
- ConfigUtil.sanitizeValuesByKey(copy, [
83
- ...this.#redactedKeys,
84
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
85
- ...(this.#get('config')?.redacted ?? []) as string[]
86
- ]) :
87
- copy;
88
- }
89
-
90
- /**
91
- * Bind config to a schema class
92
- * @param cls
93
- * @param item
94
- * @param namespace
95
- */
96
- bindTo<T>(cls: Class<T>, item: T, namespace: string): T {
97
- if (!SchemaRegistry.has(cls)) {
98
- throw new AppError(`${cls.ᚕid} is not a valid schema class, config is not supported`);
99
- }
100
-
101
- const cfg = Util.deepAssign({}, this.#get(namespace));
102
- Util.deepAssign(cfg, ConfigUtil.getEnvOverlay(cls, namespace));
103
-
104
- return BindUtil.bindSchemaToObject(cls, item, cfg);
105
- }
106
-
107
- async install<T>(cls: Class<T>, item: T, namespace: string, internal?: boolean): Promise<T> {
108
- const out = await this.bindTo(cls, item, namespace);
109
- try {
110
- await SchemaValidator.validate(cls, out);
111
- } catch (err) {
112
- if (err instanceof ValidationResultError) {
113
- err.message = `Failed to construct ${cls.ᚕid} as validation errors have occurred`;
114
- err.payload = { class: cls.ᚕid, file: cls.ᚕfile, ...(err.payload ?? {}) };
115
- }
116
- throw err;
117
- }
118
- if (out && !internal) {
119
- Util.deepAssign(ConfigUtil.lookupRoot(this.#active, namespace, true), out, 'coerce');
120
- }
121
- return out;
122
- }
123
-
124
- /**
125
- * Reset
126
- */
127
- reset(): void {
128
- this.#storage = {};
129
- this.#initialized = false;
130
- }
131
- }
132
-
133
- export const ConfigManager = new $ConfigManager();
@@ -1,11 +0,0 @@
1
- /**
2
- * Initializes the config source
3
- */
4
- export const init = {
5
- key: '@trv:config/init',
6
- before: ['@trv:registry/init'],
7
- async action(): Promise<void> {
8
- const { ConfigManager } = await import('../src/manager');
9
- await ConfigManager.init();
10
- }
11
- };