@travetto/config 3.4.0-rc.5 → 3.4.0-rc.7

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
@@ -23,11 +23,12 @@ 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#L6) - Priority `???` - These are custom config sources provided by the module, and are able to define their own priorities
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
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.
28
28
  By default all configuration data is inert, and will only be applied when constructing an instance of a configuration class.
29
29
 
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:
30
+ ### Mono Repo Support
31
+ 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
32
  1. Mono-repo root
32
33
  1. Module root
33
34
  1. Folders for `TRV_RESOURCES`, in order
@@ -96,24 +97,20 @@ The framework provides two simple base classes that assist with existing pattern
96
97
  **Code: Memory Provider**
97
98
  ```typescript
98
99
  import { ConfigData } from '../parser/types';
99
- import { ConfigSource } from './types';
100
+ import { ConfigSource, ConfigSpec } from './types';
100
101
 
101
102
  /**
102
103
  * Meant to be instantiated and provided as a unique config source
103
104
  */
104
105
  export class MemoryConfigSource implements ConfigSource {
105
- priority: number;
106
- data: ConfigData;
107
- source: string;
106
+ #spec: ConfigSpec;
108
107
 
109
108
  constructor(key: string, data: ConfigData, priority: number = 500) {
110
- this.data = data;
111
- this.priority = priority;
112
- this.source = `memory://${key}`;
109
+ this.#spec = { data, priority, source: `memory://${key}` };
113
110
  }
114
111
 
115
- getData(): ConfigData {
116
- return this.data;
112
+ get(): ConfigSpec {
113
+ return this.#spec;
117
114
  }
118
115
  }
119
116
  ```
@@ -122,27 +119,24 @@ export class MemoryConfigSource implements ConfigSource {
122
119
  ```typescript
123
120
  import { Env } from '@travetto/base';
124
121
 
125
- import { ConfigSource } from './types';
126
- import { ConfigData } from '../parser/types';
122
+ import { ConfigSource, ConfigSpec } from './types';
127
123
 
128
124
  /**
129
125
  * Represents the environment mapped data as a JSON blob
130
126
  */
131
127
  export class EnvConfigSource implements ConfigSource {
132
- priority: number;
133
- source: string;
134
128
  #envKey: string;
129
+ #priority: number;
135
130
 
136
131
  constructor(key: string, priority: number) {
137
132
  this.#envKey = key;
138
- this.priority = priority;
139
- this.source = `env://${this.#envKey}`;
133
+ this.#priority = priority;
140
134
  }
141
135
 
142
- getData(): ConfigData | undefined {
136
+ get(): ConfigSpec | undefined {
143
137
  try {
144
138
  const data = JSON.parse(Env.get(this.#envKey, '{}'));
145
- return data;
139
+ return { data, priority: this.#priority, source: `env://${this.#envKey}` };
146
140
  } catch (e) {
147
141
  console.error(`env.${this.#envKey} is an invalid format`, { text: Env.get(this.#envKey) });
148
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/config",
3
- "version": "3.4.0-rc.5",
3
+ "version": "3.4.0-rc.7",
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.5",
30
- "@travetto/schema": "^3.4.0-rc.5",
31
- "@travetto/yaml": "^3.4.0-rc.5"
29
+ "@travetto/di": "^3.4.0-rc.7",
30
+ "@travetto/schema": "^3.4.0-rc.7",
31
+ "@travetto/yaml": "^3.4.0-rc.7"
32
32
  },
33
33
  "travetto": {
34
34
  "displayName": "Configuration"
@@ -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, Env } from '@travetto/base';
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 ConfigSpec = { source: string, priority: number, detail?: string };
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: ConfigSpec[] = [];
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 specPairs = await Promise.all([
73
- new FileConfigSource(parser, 'application', 100),
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 => this.#toSpecPairs(src)));
63
+ ].map(src => src.get()));
79
64
 
80
- const specs = specPairs.flat().sort(([a], [b]) => a.priority - b.priority);
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
- this.#specs = specs.map(([v]) => v);
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: ConfigSpec[], active: ConfigData }> {
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.priority = priority;
17
- this.source = `env://${this.#envKey}`;
14
+ this.#priority = priority;
18
15
  }
19
16
 
20
- getData(): ConfigData | undefined {
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
  }
@@ -1,42 +1,54 @@
1
- import { FileQueryProvider } from '@travetto/base';
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
- import { ConfigData } from '../parser/types';
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 extends FileQueryProvider implements ConfigSource {
14
+ export class FileConfigSource implements ConfigSource {
12
15
 
13
- priority = 10;
14
- source: string;
15
- profile: string;
16
- parser: ParserManager;
16
+ #profiles: Profile[];
17
+ #searchPaths: string[];
18
+ #parser: ParserManager;
17
19
 
18
- constructor(parser: ParserManager, profile: string, priority: number, paths: string[] = []) {
19
- super({ includeCommon: true, paths });
20
- this.priority = priority;
21
- this.profile = profile;
22
- this.parser = parser;
23
- this.source = `file://${profile}`;
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 getData(): Promise<ConfigData[]> {
27
- const out: ConfigData[] = [];
28
- for await (const file of this.query(f => this.parser.matches(f))) {
29
- const ext = path.extname(file);
30
- const base = path.basename(file, ext);
31
- if (base === this.profile && !file.includes('/')) { // Ensures no nesting
32
- for (const resolved of await this.resolveAll(file)) {
33
- // Ensure more specific files are processed later, .resolveAll returns with most specific first
34
- const data = await this.parser.parse(resolved);
35
- // eslint-disable-next-line @typescript-eslint/naming-convention
36
- out.push({ ...data, __ID__: resolved.replace(`${RootIndex.manifest.workspacePath}/`, '') });
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
+ }
37
49
  }
38
50
  }
39
51
  }
40
- return out.reverse();
52
+ return Promise.all(configs);
41
53
  }
42
54
  }
@@ -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
- priority: number;
9
- data: ConfigData;
10
- source: string;
8
+ #spec: ConfigSpec;
11
9
 
12
10
  constructor(key: string, data: ConfigData, priority: number = 500) {
13
- this.data = data;
14
- this.priority = priority;
15
- this.source = `memory://${key}`;
11
+ this.#spec = { data, priority, source: `memory://${key}` };
16
12
  }
17
13
 
18
- getData(): ConfigData {
19
- return this.data;
14
+ get(): ConfigSpec {
15
+ return this.#spec;
20
16
  }
21
17
  }
@@ -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
- getData(): ConfigData {
30
- return this.#build();
26
+ get(): ConfigSpec {
27
+ return { data: this.#build(), priority: 999, source: 'memory://override' };
31
28
  }
32
29
  }
@@ -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
- priority: number;
8
- source: string;
9
- getData(): Promise<ConfigData[] | ConfigData | undefined> | ConfigData[] | ConfigData | undefined;
12
+ get(): OrProm<OneOf<ConfigSpec>>;
10
13
  }