@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 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. The general rule is that the longest path has the highest priority.
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
- priority: number;
101
- data: ConfigData;
102
- source: string;
105
+ #spec: ConfigSpec;
103
106
 
104
107
  constructor(key: string, data: ConfigData, priority: number = 500) {
105
- this.data = data;
106
- this.priority = priority;
107
- this.source = `memory://${key}`;
108
+ this.#spec = { data, priority, source: `memory://${key}` };
108
109
  }
109
110
 
110
- getData(): ConfigData {
111
- return this.data;
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.priority = priority;
134
- this.source = `env://${this.#envKey}`;
132
+ this.#priority = priority;
135
133
  }
136
134
 
137
- getData(): ConfigData | undefined {
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.4",
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.4",
30
- "@travetto/schema": "^3.4.0-rc.4",
31
- "@travetto/yaml": "^3.4.0-rc.4"
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"
@@ -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,44 +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: { file: string, data: 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
- out.push({ file: resolved, data: await this.parser.parse(resolved) });
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
  }
@@ -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
  }