@travetto/config 3.0.0-rc.8 → 3.0.0

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
@@ -6,6 +6,10 @@
6
6
  **Install: @travetto/config**
7
7
  ```bash
8
8
  npm install @travetto/config
9
+
10
+ # or
11
+
12
+ yarn add @travetto/config
9
13
  ```
10
14
 
11
15
  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
@@ -16,26 +20,24 @@ entrypoint into the application. Given that all [@Config](https://github.com/tr
16
20
  The configuration information is comprised of:
17
21
 
18
22
 
19
- * [YAML](https://en.wikipedia.org/wiki/YAML) files
20
- * environment variables
23
+ * configuration files - [YAML](https://en.wikipedia.org/wiki/YAML), [JSON](https://www.json.org), and basic properties file
21
24
  * configuration classes
22
25
 
23
- Config loading follows a defined resolution path, below is the order in increasing specificity:
26
+ Config loading follows a defined resolution path, below is the order in increasing specificity (`ext` can be `yaml`, `yml`, `json`, `properties`):
24
27
 
25
- 1. `resources/application.yml` - Load the default `application.yml` if available.
26
- 1. `resources/*.yml` - Load profile specific configurations as defined by the values in `process.env.TRV_PROFILES`
27
- 1. `resources/{env}.yml` - Load environment specific profile configurations as defined by the values of `process.env.TRV_ENV`.
28
- 1. `process.env` - Read startup configuration from environment to allow for overriding any values. Because we are overriding a [YAML](https://en.wikipedia.org/wiki/YAML) based configuration we need to compensate for the differences in usage patterns. Generally all environment variables are passed in as `UPPER_SNAKE_CASE`. When reading from `process.env` we will map `UPPER_SNAKE_CASE` to `upper.snake.case`, and will attempt to match by case-insensitive name.
28
+ 1. `resources/application.<ext>` - Load the default `application.<ext>` if available.
29
+ 1. `resources/*.<ext>` - Load profile specific configurations as defined by the values in `process.env.TRV_PROFILES`
30
+ 1. `resources/{env}.<ext>` - Load environment specific profile configurations as defined by the values of `process.env.TRV_ENV`.
29
31
 
30
- By default all configuration data is inert, and will only be applied when constructing an instance of a configuration class. This is due to the fact that environment data, as well as configuration data can only be interpreted in light of a class structure, as the data binding is what makes the configuration valid.
32
+ By default all configuration data is inert, and will only be applied when constructing an instance of a configuration class.
31
33
 
32
- ## A Complete Example
34
+ ### A Complete Example
33
35
 
34
36
  A more complete example setup would look like:
35
37
 
36
38
  **Config: resources/application.yml**
37
39
  ```yaml
38
- --
40
+ ---
39
41
  database:
40
42
  host: localhost
41
43
  creds:
@@ -43,13 +45,16 @@ database:
43
45
  password: test
44
46
  ```
45
47
 
46
- **Config: resources/prod.yml**
47
- ```yaml
48
- --
49
- database:
50
- host: prod - host - db
51
- creds:
52
- user: admin - user
48
+ **Config: resources/prod.json**
49
+ ```json
50
+ {
51
+ "database": {
52
+ "host": "prod-host-db",
53
+ "creds": {
54
+ "user": "admin-user"
55
+ }
56
+ }
57
+ }
53
58
  ```
54
59
 
55
60
  with environment variables
@@ -58,38 +63,61 @@ with environment variables
58
63
  ```properties
59
64
  TRV_ENV = prod
60
65
  TRV_PROFILES = prod
61
- DATABASE_PORT = 1234
62
- DATABASE_CREDS_PASSWORD = %secret%
63
66
  ```
64
67
 
65
68
  At runtime the resolved config would be:
66
69
 
67
70
  **Terminal: Runtime Resolution**
68
71
  ```bash
69
- $ trv main support/main.resolve.ts
72
+ $ trv main doc/resolve.ts
70
73
 
71
74
  Config {
72
75
  sources: [
73
- 'application.1 - file://application.yml',
76
+ 'application.1 - file://./doc/resources/application.yml',
77
+ 'prod.1 - file://./doc/resources/prod.json',
74
78
  'override.3 - memory://override'
75
79
  ],
76
80
  active: {
77
- DBConfig: {
78
- host: 'localhost',
79
- port: 2000,
80
- creds: creds_7_45Ⲑsyn { user: 'test', password: 'test' }
81
- }
81
+ DBConfig: { host: 'prod-host-db', port: 2000, creds: { user: 'admin-user' } }
82
82
  }
83
83
  }
84
- 
85
84
  ```
86
85
 
87
- ## Secrets
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`.
86
+ ### Custom Configuration Provider
87
+ In addition to files and environment variables, configuration sources can also be provided via the class itself. This is useful for reading remote configurations, or dealing with complex configuration normalization. The only caveat to this pattern, is that the these configuration sources cannot rely on the [Configuration](https://github.com/travetto/travetto/tree/main/module/config/src/configuration.ts#L14) service for input. This means any needed configuration will need to be accessed via specific patterns.
88
+
89
+ **Code: Custom Configuration Source**
90
+ ```typescript
91
+ import { ConfigSource, ConfigValue } from '@travetto/config';
92
+ import { Injectable } from '@travetto/di';
93
+
94
+ @Injectable()
95
+ export class CustomConfigSource implements ConfigSource {
96
+ priority = 1000;
97
+ name = 'custom';
98
+
99
+ async getValues(): Promise<ConfigValue[]> {
100
+ return [
101
+ {
102
+ config: { user: { name: 'bob' } },
103
+ priority: this.priority,
104
+ profile: 'override',
105
+ source: `custom://${CustomConfigSource.name}`
106
+ }
107
+ ];
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Startup
113
+ At startup, the [Configuration](https://github.com/travetto/travetto/tree/main/module/config/src/configuration.ts#L14) service will log out all the registered configuration objects. The configuration state output is useful to determine if everything is configured properly when diagnosing runtime errors. This service will find all configurations, and output a redacted version with all secrets removed. The default pattern for secrets is `/password|private|secret/i`. More values can be added in your configuration under the path `config.secrets`. These values can either be simple strings (for exact match), or `/pattern/` to create a regular expression.
89
114
 
90
115
  ## Consuming
91
116
  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.
92
117
 
118
+ ### Environment Variables
119
+ Additionally there are times in which you may want to also support configuration via environment variables. [EnvVar](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L34) supports override configuration values when environment variables are present.
120
+
93
121
  The decorator takes in a namespace, of what part of the resolved configuration you want to bind to your class. Given the following class:
94
122
 
95
123
  **Code: Database config object**
@@ -108,13 +136,29 @@ export class DBConfig {
108
136
  }
109
137
  ```
110
138
 
111
- Using the above config files, you'll notice that the port is not specified (its only specified in the environment variables). This means when the application attempts to start up, it will fail if the port is not specified via an environment variable:
139
+ You can see that the `DBConfig` allows for the `port` to be overridden by the `DATABASE_PORT` environment variable.
112
140
 
113
141
  **Terminal: Resolved database config**
114
142
  ```bash
115
- $ trv main support/main.dbconfig-run.ts
116
-
117
- 
143
+ $ trv main doc/dbconfig-run.ts
144
+
145
+ {
146
+ message: 'Failed to construct @travetto/config:doc/dbconfig○DBConfig as validation errors have occurred',
147
+ category: 'data',
148
+ type: 'ValidationResultError',
149
+ at: 2029-03-14T04:00:00.618Z,
150
+ class: '@travetto/config:doc/dbconfig○DBConfig',
151
+ file: './doc/dbconfig.ts',
152
+ errors: [
153
+ {
154
+ kind: 'required',
155
+ value: undefined,
156
+ message: 'port is required',
157
+ path: 'port',
158
+ type: undefined
159
+ }
160
+ ]
161
+ }
118
162
  ```
119
163
 
120
164
  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.
@@ -123,20 +167,18 @@ By passing in the port via the environment variable, the config will construct p
123
167
 
124
168
  **Terminal: Resolved database config**
125
169
  ```bash
126
- $ trv main support/main.dbconfig-run.ts
170
+ $ DATABASE_PORT=200 trv main doc/dbconfig-run.ts
127
171
 
128
172
  Config {
129
173
  sources: [
130
- 'application.1 - file://application.yml',
174
+ 'application.1 - file://./doc/resources/application.yml',
175
+ 'prod.1 - file://./doc/resources/prod.json',
131
176
  'override.3 - memory://override'
132
177
  ],
133
178
  active: {
134
- DBConfig: {
135
- host: 'localhost',
136
- port: 200,
137
- creds: creds_7_45Ⲑsyn { user: 'test', password: 'test' }
138
- }
179
+ DBConfig: { host: 'prod-host-db', port: 200, creds: { user: 'admin-user' } }
139
180
  }
140
181
  }
141
- 
142
182
  ```
183
+
184
+ Additionally you may notice that the `password` field is missing, as it is redacted by default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/config",
3
- "version": "3.0.0-rc.8",
3
+ "version": "3.0.0",
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.0.0-rc.8",
30
- "@travetto/schema": "^3.0.0-rc.8",
31
- "@travetto/yaml": "^3.0.0-rc.6"
29
+ "@travetto/di": "^3.0.0",
30
+ "@travetto/schema": "^3.0.0",
31
+ "@travetto/yaml": "^3.0.0"
32
32
  },
33
33
  "travetto": {
34
34
  "displayName": "Configuration"
@@ -26,6 +26,7 @@ export class Configuration {
26
26
  #storage: Record<string, unknown> = {}; // Lowered, and flattened
27
27
  #profiles: string[] = ['application', ...GlobalEnv.profiles, 'override'];
28
28
  #sources: string[] = [];
29
+ #secrets: (RegExp | string)[] = [/password|private|secret/i];
29
30
 
30
31
  /**
31
32
  * Get a sub tree of the config, or everything if namespace is not passed
@@ -67,6 +68,18 @@ export class Configuration {
67
68
  for (const { config: element } of sorted) {
68
69
  DataUtil.deepAssign(this.#storage, BindUtil.expandPaths(element), 'coerce');
69
70
  }
71
+
72
+ // Initialize Secrets
73
+ const userSpecified = (this.#get('config')?.secrets ?? []);
74
+ for (const el of Array.isArray(userSpecified) ? userSpecified : [userSpecified]) {
75
+ if (el !== undefined && el !== null && typeof el === 'string') {
76
+ if (el.startsWith('/')) {
77
+ this.#secrets.push(DataUtil.coerceType(el, RegExp, true));
78
+ } else {
79
+ this.#secrets.push(DataUtil.coerceType(el, String, true));
80
+ }
81
+ }
82
+ }
70
83
  }
71
84
 
72
85
  /**
@@ -89,7 +102,7 @@ export class Configuration {
89
102
  const data = BindUtil.bindSchemaToObject<ConfigData>(
90
103
  inst.constructor, {}, inst, { filterField: f => !f.secret, filterValue: v => v !== undefined }
91
104
  );
92
- out[el.class.name] = data;
105
+ out[el.class.name] = DataUtil.filterByKeys(data, this.#secrets);
93
106
  }
94
107
  return { sources: this.#sources, active: out };
95
108
  }
@@ -49,10 +49,11 @@ export class FileConfigSource extends FileQueryProvider implements ConfigSource
49
49
  continue;
50
50
  }
51
51
  const content = await this.read(file);
52
+ const desc = await this.describe(file);
52
53
  out.push({
53
54
  profile,
54
55
  config: await this.parsers[ext].parse(content),
55
- source: `file://${file}`,
56
+ source: `file://${desc.path}`,
56
57
  priority: this.priority
57
58
  });
58
59
  }