@spinajs/configuration 2.0.556 → 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.
Files changed (89) hide show
  1. package/lib/cjs/config/configuration.d.ts +1 -5
  2. package/lib/cjs/config/configuration.d.ts.map +1 -1
  3. package/lib/cjs/config/configuration.js +3 -1
  4. package/lib/cjs/config/configuration.js.map +1 -1
  5. package/lib/cjs/configuration.d.ts +24 -1
  6. package/lib/cjs/configuration.d.ts.map +1 -1
  7. package/lib/cjs/configuration.js +357 -82
  8. package/lib/cjs/configuration.js.map +1 -1
  9. package/lib/cjs/decorators.d.ts +77 -4
  10. package/lib/cjs/decorators.d.ts.map +1 -1
  11. package/lib/cjs/decorators.js +212 -11
  12. package/lib/cjs/decorators.js.map +1 -1
  13. package/lib/cjs/exception.d.ts +9 -1
  14. package/lib/cjs/exception.d.ts.map +1 -1
  15. package/lib/cjs/exception.js +2 -1
  16. package/lib/cjs/exception.js.map +1 -1
  17. package/lib/cjs/helpers.d.ts.map +1 -1
  18. package/lib/cjs/helpers.js +3 -2
  19. package/lib/cjs/helpers.js.map +1 -1
  20. package/lib/cjs/index.browser.d.ts +1 -0
  21. package/lib/cjs/index.browser.d.ts.map +1 -1
  22. package/lib/cjs/index.browser.js +4 -0
  23. package/lib/cjs/index.browser.js.map +1 -1
  24. package/lib/cjs/index.d.ts +3 -0
  25. package/lib/cjs/index.d.ts.map +1 -1
  26. package/lib/cjs/index.js +6 -0
  27. package/lib/cjs/index.js.map +1 -1
  28. package/lib/cjs/lib-dirs.d.ts +8 -0
  29. package/lib/cjs/lib-dirs.d.ts.map +1 -0
  30. package/lib/cjs/lib-dirs.js +16 -0
  31. package/lib/cjs/lib-dirs.js.map +1 -0
  32. package/lib/cjs/schemas.d.ts +20 -0
  33. package/lib/cjs/schemas.d.ts.map +1 -0
  34. package/lib/cjs/schemas.js +58 -0
  35. package/lib/cjs/schemas.js.map +1 -0
  36. package/lib/cjs/sources.d.ts +9 -3
  37. package/lib/cjs/sources.d.ts.map +1 -1
  38. package/lib/cjs/sources.js +27 -10
  39. package/lib/cjs/sources.js.map +1 -1
  40. package/lib/cjs/util.d.ts +8 -0
  41. package/lib/cjs/util.d.ts.map +1 -1
  42. package/lib/cjs/util.js +7 -1
  43. package/lib/cjs/util.js.map +1 -1
  44. package/lib/mjs/config/configuration.d.ts +1 -5
  45. package/lib/mjs/config/configuration.d.ts.map +1 -1
  46. package/lib/mjs/config/configuration.js +3 -1
  47. package/lib/mjs/config/configuration.js.map +1 -1
  48. package/lib/mjs/configuration.d.ts +24 -1
  49. package/lib/mjs/configuration.d.ts.map +1 -1
  50. package/lib/mjs/configuration.js +326 -84
  51. package/lib/mjs/configuration.js.map +1 -1
  52. package/lib/mjs/decorators.d.ts +77 -4
  53. package/lib/mjs/decorators.d.ts.map +1 -1
  54. package/lib/mjs/decorators.js +207 -12
  55. package/lib/mjs/decorators.js.map +1 -1
  56. package/lib/mjs/exception.d.ts +9 -1
  57. package/lib/mjs/exception.d.ts.map +1 -1
  58. package/lib/mjs/exception.js +2 -1
  59. package/lib/mjs/exception.js.map +1 -1
  60. package/lib/mjs/helpers.d.ts.map +1 -1
  61. package/lib/mjs/helpers.js +3 -2
  62. package/lib/mjs/helpers.js.map +1 -1
  63. package/lib/mjs/index.browser.d.ts +1 -0
  64. package/lib/mjs/index.browser.d.ts.map +1 -1
  65. package/lib/mjs/index.browser.js +1 -0
  66. package/lib/mjs/index.browser.js.map +1 -1
  67. package/lib/mjs/index.d.ts +3 -0
  68. package/lib/mjs/index.d.ts.map +1 -1
  69. package/lib/mjs/index.js +3 -0
  70. package/lib/mjs/index.js.map +1 -1
  71. package/lib/mjs/lib-dirs.d.ts +8 -0
  72. package/lib/mjs/lib-dirs.d.ts.map +1 -0
  73. package/lib/mjs/lib-dirs.js +13 -0
  74. package/lib/mjs/lib-dirs.js.map +1 -0
  75. package/lib/mjs/schemas.d.ts +20 -0
  76. package/lib/mjs/schemas.d.ts.map +1 -0
  77. package/lib/mjs/schemas.js +54 -0
  78. package/lib/mjs/schemas.js.map +1 -0
  79. package/lib/mjs/sources.d.ts +9 -3
  80. package/lib/mjs/sources.d.ts.map +1 -1
  81. package/lib/mjs/sources.js +27 -10
  82. package/lib/mjs/sources.js.map +1 -1
  83. package/lib/mjs/util.d.ts +8 -0
  84. package/lib/mjs/util.d.ts.map +1 -1
  85. package/lib/mjs/util.js +4 -0
  86. package/lib/mjs/util.js.map +1 -1
  87. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  88. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  89. package/package.json +7 -7
@@ -1,10 +1,43 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
2
18
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
19
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
20
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
21
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
22
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
23
  };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  var __metadata = (this && this.__metadata) || function (k, v) {
9
42
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
43
  };
@@ -29,13 +62,77 @@ const configuration_common_1 = require("@spinajs/configuration-common");
29
62
  const di_1 = require("@spinajs/di");
30
63
  const exception_js_1 = require("./exception.js");
31
64
  const util_js_1 = require("./util.js");
32
- const configuration_js_1 = __importDefault(require("./config/configuration.js"));
65
+ const configuration_js_1 = __importStar(require("./config/configuration.js"));
33
66
  require("./sources.js");
34
67
  const ajv_merge_patch_1 = __importDefault(require("ajv-merge-patch"));
35
68
  const ajv_formats_1 = __importDefault(require("ajv-formats"));
36
69
  const ajv_keywords_1 = __importDefault(require("ajv-keywords"));
37
70
  const PROTOCOL_REGEXP = /^([a-zA-Z0-9\-]+:\/\/)+(.+)$/gm;
38
71
  const COMMON_WEB_PROTOCOLS = ['http://', 'https://', 'ws://', 'wss://', 'ftp://', 'ftps://', 'file://', 'data://', 'blob://', 'mailto:'];
72
+ // A Configuration whose check ran but which DI has not cached yet (the microtasks after resolve() returns), by container.
73
+ const CHECKED = new WeakMap();
74
+ // The target of each lazy-var proxy load() builds, by proxy: copying a proxy property by property would load every var.
75
+ const LAZY_TARGETS = new WeakMap();
76
+ // Set while Ajv validates the stored value in place: it writes coerced values back through the proxies.
77
+ let keepUnresolvedVars = false;
78
+ const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
79
+ // _.set skips these keys, but _.get resolves them, so merge() into `__proto__` would write to Object.prototype.
80
+ function assertSafePath(path) {
81
+ const unsafe = lodash_1.default.toPath(path).find((s) => UNSAFE_PATH_SEGMENTS.has(s));
82
+ if (unsafe !== undefined)
83
+ throw new exceptions_1.InvalidArgument(`Configuration path '${lodash_1.default.toPath(path).join('.')}' contains the reserved segment '${unsafe}'`);
84
+ }
85
+ const isUnresolved = (v) => v instanceof configuration_common_1.ConfigVar && (v.get() ?? null) === null;
86
+ function withLazyVars(obj) {
87
+ const proxy = new Proxy(obj, {
88
+ get: (o, prop) => (o[prop] instanceof configuration_common_1.ConfigVar ? o[prop].get() : o[prop]),
89
+ set: (o, prop, value) => {
90
+ // A var that is not ready yet reads as null; its coercion ('' for a string) must not replace it for good.
91
+ if (!(keepUnresolvedVars && isUnresolved(o[prop])))
92
+ o[prop] = value;
93
+ return true;
94
+ },
95
+ });
96
+ LAZY_TARGETS.set(proxy, obj);
97
+ return proxy;
98
+ }
99
+ // A copy that validates as the stored value does: behind a copied lazy-var proxy only the vars the schema reads load,
100
+ // and a class instance (a ConfigVar, one with #private fields a copy would lack) is kept by reference.
101
+ function copyForCheck(value) {
102
+ return lodash_1.default.cloneDeepWith(value, function copy(v) {
103
+ const target = typeof v === 'object' && v !== null ? LAZY_TARGETS.get(v) : undefined;
104
+ if (target)
105
+ return withLazyVars(lodash_1.default.cloneDeepWith(target, copy));
106
+ if (Object.prototype.toString.call(v) === '[object Object]' && !lodash_1.default.isPlainObject(v))
107
+ return v;
108
+ return undefined;
109
+ });
110
+ }
111
+ const decodePointer = (segment) => segment.replace(/~1/g, '/').replace(/~0/g, '~');
112
+ // Ajv tags the sub-errors of a propertyNames check with `propertyName`; the check itself reports the key in `params`.
113
+ const isNameError = (e) => e.propertyName !== undefined;
114
+ const keyOf = (e) => {
115
+ const p = e.params;
116
+ return p.missingProperty ?? p.additionalProperty ?? p.propertyName;
117
+ };
118
+ function toViolations(module, schema, errors) {
119
+ const violations = [];
120
+ for (const e of errors) {
121
+ if (isNameError(e))
122
+ continue;
123
+ const segments = e.instancePath.split('/').filter((p) => p !== '').map(decodePointer);
124
+ const key = keyOf(e);
125
+ if (key !== undefined)
126
+ segments.push(String(key));
127
+ let message = e.message ?? e.keyword;
128
+ if (e.keyword === 'propertyNames') {
129
+ const detail = errors.filter((n) => isNameError(n) && n.instancePath === e.instancePath && n.propertyName === key).map((n) => n.message ?? n.keyword);
130
+ message = `property name is invalid${detail.length ? `: ${detail.join(', ')}` : ''}`;
131
+ }
132
+ violations.push({ path: [module, ...segments].join('.'), schema, message });
133
+ }
134
+ return violations;
135
+ }
39
136
  /**
40
137
  * HACK:
41
138
  * Becouse of ajv not supporting esm default exports we need to
@@ -65,6 +162,9 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
65
162
  * Loaded & merged configuration
66
163
  */
67
164
  this.Config = {};
165
+ /** Schemas applied after boot that refused the loaded value, by schema; a read of their module throws. */
166
+ this.RefusedModules = new Map();
167
+ this.Loading = false;
68
168
  this.CustomConfigPaths = options?.cfgCustomPaths ?? [];
69
169
  this.RunApp = options?.app ?? (0, util_js_1.parseArgv)('--app') ?? '';
70
170
  this.AppBaseDir = options?.appBaseDir ?? (0, util_js_1.parseArgv)('--apppath') ?? (0, path_1.join)(process.cwd(), '../apps/');
@@ -77,7 +177,33 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
77
177
  * @param defaultValue - optional, if value at specified path not exists returns default value
78
178
  */
79
179
  get(path, defaultValue) {
80
- return lodash_1.default.get(this.Config, path, defaultValue);
180
+ if (this.RefusedModules.size !== 0)
181
+ this.throwIfRefused(path);
182
+ const value = lodash_1.default.get(this.Config, path, defaultValue);
183
+ if (value === null && defaultValue !== undefined && this.isUnresolvedVar(path))
184
+ return defaultValue;
185
+ return value;
186
+ }
187
+ isUnresolvedVar(path) {
188
+ const segments = lodash_1.default.toPath(path);
189
+ if (segments.length === 0)
190
+ return false;
191
+ const parent = segments.length === 1 ? this.Config : lodash_1.default.get(this.Config, segments.slice(0, -1));
192
+ const target = typeof parent === 'object' && parent !== null ? LAZY_TARGETS.get(parent) : undefined;
193
+ return target !== undefined && isUnresolved(target[segments[segments.length - 1]]);
194
+ }
195
+ // A schema applied after boot cannot fail the boot; reading the value it refused, or a part of it, must still fail.
196
+ throwIfRefused(path) {
197
+ const segments = lodash_1.default.toPath(path);
198
+ // An empty path names no module (and reads nothing): it must not match every refused one.
199
+ if (segments.length === 0)
200
+ return;
201
+ for (const { module, error } of this.RefusedModules.values()) {
202
+ const n = Math.min(module.length, segments.length);
203
+ if (lodash_1.default.isEqual(module.slice(0, n), segments.slice(0, n))) {
204
+ throw new exception_js_1.InvalidConfiguration(error.message, error.validationExceptions, error.violations);
205
+ }
206
+ }
81
207
  }
82
208
  /**
83
209
  * Sets at given path configuration value. Use when you want to override config
@@ -87,7 +213,11 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
87
213
  * @param value - value to set
88
214
  */
89
215
  set(path, value) {
216
+ assertSafePath(path);
217
+ const schemas = this.schemasAt(path);
218
+ this.checkChange(schemas, path, (root) => lodash_1.default.set(root, path, copyForCheck(value)));
90
219
  this.Config = lodash_1.default.set(this.Config, path, value);
220
+ this.applySchemas(schemas);
91
221
  }
92
222
  /**
93
223
  * Merge existing config value with new options instead overriding
@@ -96,7 +226,11 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
96
226
  * @param value - value to merge
97
227
  */
98
228
  merge(path, value) {
229
+ assertSafePath(path);
230
+ const schemas = this.schemasAt(path);
231
+ this.checkChange(schemas, path, (root) => lodash_1.default.mergeWith(lodash_1.default.get(root, path), copyForCheck(value), util_js_1.mergeArrays));
99
232
  lodash_1.default.mergeWith(this.get(path), value, util_js_1.mergeArrays);
233
+ this.applySchemas(schemas);
100
234
  }
101
235
  async resolve() {
102
236
  await super.resolve();
@@ -110,48 +244,76 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
110
244
  throw new exceptions_1.InvalidArgument(`ConfigurationConfigPaths should be an array`);
111
245
  }
112
246
  this.CustomConfigPaths.forEach((p) => {
113
- internal_logger_1.InternalLogger.trace(`Custom config path at: ${p}`, 'Configuration');
247
+ internal_logger_1.InternalLogger.trace({ path: p }, 'Custom config path at: ${path}', 'Configuration');
114
248
  });
115
249
  }
116
250
  if (this.RunApp) {
117
- internal_logger_1.InternalLogger.info(`Run app is ${this.RunApp}`, 'Configuration');
251
+ internal_logger_1.InternalLogger.info({ runApp: this.RunApp }, 'Run app is ${runApp}', 'Configuration');
118
252
  }
119
- internal_logger_1.InternalLogger.info(`App base dir is ${this.AppBaseDir}`, 'Configuration');
253
+ internal_logger_1.InternalLogger.info({ appBaseDir: this.AppBaseDir }, 'App base dir is ${appBaseDir}', 'Configuration');
120
254
  this.initValidator();
121
- this.applyAppDirs();
122
- // add default configuration of this module
123
- // so we dont need to import it
124
- this.set('configuration', configuration_js_1.default);
125
255
  /**
126
256
  * Load and validate data from cfg sources
127
257
  * in proper order
128
258
  */
129
259
  this.loadSources();
130
260
  await this.load();
131
- this.validate();
132
- this.configure();
261
+ try {
262
+ this.validate();
263
+ this.configure();
264
+ }
265
+ catch (e) {
266
+ // DI does not cache a Configuration whose resolve failed: a schema registered later must not be checked against it.
267
+ if (CHECKED.get(this.Container) === this)
268
+ CHECKED.delete(this.Container);
269
+ throw e;
270
+ }
133
271
  }
134
272
  async mergeSource(sType) {
135
273
  const source = this.Container.resolve(sType);
136
274
  const sCfg = await source.Load(this);
275
+ const schemas = sCfg ? lodash_1.default.uniq(Object.keys(sCfg).flatMap((k) => this.schemasAt([k]))) : [];
276
+ this.checkChange(schemas, [], (root) => lodash_1.default.mergeWith(root, copyForCheck(sCfg), util_js_1.mergeArrays));
137
277
  this.Sources.push(source);
138
278
  if (sCfg) {
139
- lodash_1.default.mergeWith(sCfg, util_js_1.mergeArrays);
279
+ lodash_1.default.mergeWith(this.Config, sCfg, util_js_1.mergeArrays);
140
280
  }
281
+ this.applySchemas(schemas);
141
282
  }
142
283
  /**
143
284
  * Reloads configuration data
144
285
  */
145
286
  async load() {
287
+ const previous = this.Config;
288
+ this.Loading = true;
289
+ try {
290
+ await this.loadTree();
291
+ // resolve() checks the first load itself.
292
+ if (this.ValidationSchemas !== undefined)
293
+ this.checkAll();
294
+ }
295
+ catch (e) {
296
+ this.Config = previous;
297
+ throw e;
298
+ }
299
+ finally {
300
+ this.Loading = false;
301
+ }
302
+ }
303
+ async loadTree() {
146
304
  this.Config = {};
305
+ this.NamedEnv = this.Env ?? this.Container.get('process.env')?.APP_ENV ?? process.env.NODE_ENV;
147
306
  // add env variables to config
148
- const env = di_1.DI.get('process.env');
307
+ const env = this.Container.get('process.env');
308
+ const appEnv = this.Env ?? env?.APP_ENV ?? process.env.NODE_ENV ?? 'development';
149
309
  this.set('process.env', {
150
310
  ...process.env,
151
311
  ...env,
152
- APP_ENV: this.Env ?? env?.APP_ENV ?? process.env.NODE_ENV ?? 'development',
312
+ APP_ENV: appEnv,
153
313
  });
154
- internal_logger_1.InternalLogger.info(`APP_ENV set to ${this.get('process.env.APP_ENV')}`, 'Configuration');
314
+ // Defaults of this module, which sources may override. The flags follow the APP_ENV that selects the config files.
315
+ lodash_1.default.mergeWith(this.Config, lodash_1.default.cloneDeep(configuration_js_1.default), { configuration: (0, configuration_js_1.buildEnvironmentFlags)({ APP_ENV: appEnv }) }, util_js_1.mergeArrays);
316
+ internal_logger_1.InternalLogger.info({ appEnv: this.get('process.env.APP_ENV') }, 'APP_ENV set to ${appEnv}', 'Configuration');
155
317
  for (const source of this.Sources) {
156
318
  const rCfg = await source.Load(this);
157
319
  if (rCfg) {
@@ -164,11 +326,11 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
164
326
  * eg. when custom modules have config and dont want to use files
165
327
  * eg. in webpack environment
166
328
  */
329
+ // Merged at the root: merge() writes nothing at a key no source defines.
167
330
  this.Container.resolve(Array.ofType('__configuration__')).forEach((c) => {
168
- Object.keys(c).forEach((k) => {
169
- this.merge(k, c[`${k}`]);
170
- });
331
+ lodash_1.default.mergeWith(this.Config, c, util_js_1.mergeArrays);
171
332
  });
333
+ this.applyAppDirs();
172
334
  // load config vars from various protocols
173
335
  await this.loadProtocolVars();
174
336
  /**
@@ -185,14 +347,7 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
185
347
  if (!haveProtoVals) {
186
348
  return obj;
187
349
  }
188
- return new Proxy(obj, {
189
- get: (obj, prop) => {
190
- if (obj[prop] instanceof configuration_common_1.ConfigVar) {
191
- return obj[prop].get();
192
- }
193
- return obj[prop];
194
- },
195
- });
350
+ return withLazyVars(obj);
196
351
  };
197
352
  // omit system config prop couse dir dynamic loading.
198
353
  // NOTE: the root object must be wrapped by proxyFunc AFTER merging - spreading
@@ -205,7 +360,7 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
205
360
  });
206
361
  }
207
362
  async loadProtocolVars() {
208
- const configProtocols = await di_1.DI.resolve(Array.ofType(configuration_common_1.ConfigVarProtocol));
363
+ const configProtocols = await this.Container.resolve(Array.ofType(configuration_common_1.ConfigVarProtocol));
209
364
  if (!configProtocols || configProtocols.length === 0) {
210
365
  internal_logger_1.InternalLogger.warn(`No configuration protocols registered`, 'Configuration');
211
366
  return;
@@ -213,54 +368,55 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
213
368
  // Build protocol map for O(1) lookup
214
369
  const protocolMap = new Map();
215
370
  configProtocols.forEach((p) => {
216
- internal_logger_1.InternalLogger.info(`Registered configuration protocol: ${p.Protocol}`, 'Configuration');
371
+ internal_logger_1.InternalLogger.info({ protocol: p.Protocol }, 'Registered configuration protocol: ${protocol}', 'Configuration');
217
372
  protocolMap.set(p.Protocol, p);
218
373
  });
219
374
  // Build common web protocol prefix set for fast checking
220
375
  const webProtocolSet = new Set(COMMON_WEB_PROTOCOLS.map(p => p.toLowerCase()));
376
+ const resolveString = async (obj, key, val, currentPath) => {
377
+ // Quick check: does it contain ://?
378
+ const protocolIndex = val.indexOf('://');
379
+ if (protocolIndex === -1) {
380
+ return;
381
+ }
382
+ // Extract potential protocol (everything before ://)
383
+ const potentialProtocol = val.substring(0, protocolIndex + 3).toLowerCase();
384
+ // Skip common web protocols using Set lookup (O(1))
385
+ if (webProtocolSet.has(potentialProtocol)) {
386
+ return;
387
+ }
388
+ // Use regex only if we passed initial checks
389
+ PROTOCOL_REGEXP.lastIndex = 0;
390
+ const match = PROTOCOL_REGEXP.exec(val);
391
+ if (!match) {
392
+ return;
393
+ }
394
+ // O(1) protocol lookup using Map
395
+ const protocol = protocolMap.get(match[1]);
396
+ if (!protocol) {
397
+ internal_logger_1.InternalLogger.warn({ protocol: match[1] }, 'Protocol ${protocol} used in configuration is not registered.', 'Configuration');
398
+ return;
399
+ }
400
+ const fullPath = [...currentPath, key].join('.');
401
+ try {
402
+ obj[key] = await protocol.getVar(match[2], this.Config);
403
+ }
404
+ catch (err) {
405
+ internal_logger_1.InternalLogger.error({ error: err, key: fullPath, protocol: protocol.Protocol, value: val }, 'Error loading configuration var for key ${key} using protocol ${protocol}, value: ${value}', 'Configuration');
406
+ }
407
+ };
221
408
  const iterate = async (obj, currentPath = []) => {
222
409
  if (!obj) {
223
410
  return;
224
411
  }
225
412
  if (Array.isArray(obj)) {
226
- await Promise.all(obj.map((c, index) => iterate(c, [...currentPath, `[${index}]`])));
413
+ await Promise.all(obj.map((c, index) => (typeof c === 'string' ? resolveString(obj, String(index), c, currentPath) : iterate(c, [...currentPath, `[${index}]`]))));
227
414
  return;
228
415
  }
229
416
  const stringEntries = (0, util_js_1.pickString)(obj);
230
417
  const objectEntries = (0, util_js_1.pickObjects)(obj);
231
418
  // Process strings in parallel
232
- await Promise.all(stringEntries.map(async ([key, val]) => {
233
- // Quick check: does it contain ://?
234
- const protocolIndex = val.indexOf('://');
235
- if (protocolIndex === -1) {
236
- return;
237
- }
238
- // Extract potential protocol (everything before ://)
239
- const potentialProtocol = val.substring(0, protocolIndex + 3).toLowerCase();
240
- // Skip common web protocols using Set lookup (O(1))
241
- if (webProtocolSet.has(potentialProtocol)) {
242
- return;
243
- }
244
- // Use regex only if we passed initial checks
245
- PROTOCOL_REGEXP.lastIndex = 0;
246
- const match = PROTOCOL_REGEXP.exec(val);
247
- if (!match) {
248
- return;
249
- }
250
- // O(1) protocol lookup using Map
251
- const protocol = protocolMap.get(match[1]);
252
- if (!protocol) {
253
- internal_logger_1.InternalLogger.warn(`Protocol ${match[1]} used in configuration is not registered.`, 'Configuration');
254
- return;
255
- }
256
- const fullPath = [...currentPath, key].join('.');
257
- try {
258
- obj[key] = await protocol.getVar(match[2], this.Config);
259
- }
260
- catch (err) {
261
- internal_logger_1.InternalLogger.error(err, `Error loading configuration var for key ${fullPath} using protocol ${protocol.Protocol}, value: ${val}`, 'Configuration');
262
- }
263
- }));
419
+ await Promise.all(stringEntries.map(([key, val]) => resolveString(obj, key, val, currentPath)));
264
420
  // Process nested objects in parallel
265
421
  await Promise.all(objectEntries.map(([key, val]) => iterate(val, [...currentPath, key])));
266
422
  };
@@ -282,21 +438,130 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
282
438
  });
283
439
  }
284
440
  validate() {
285
- this.ValidationSchemas.forEach((s) => {
286
- const config = this.get(s.$configurationModule);
287
- if (!config) {
288
- internal_logger_1.InternalLogger.warn(`Cannot validate configuration for module ${s.$configurationModule}, configuration is not set`, 'Configuration');
289
- return;
441
+ this.checkAll();
442
+ const container = this.Container;
443
+ CHECKED.set(container, this);
444
+ container.once('di.resolved.Configuration', () => {
445
+ if (CHECKED.get(container) === this)
446
+ CHECKED.delete(container);
447
+ });
448
+ }
449
+ /** The Configuration of `container` that has run its check but is not resolved (cached) yet. */
450
+ static checkedIn(container) {
451
+ return CHECKED.get(container);
452
+ }
453
+ checkAll() {
454
+ this.ValidationSchemas = this.schemasOfLineage();
455
+ const errors = [];
456
+ const violations = [];
457
+ for (const s of this.ValidationSchemas) {
458
+ const r = this.checkModule(s);
459
+ errors.push(...r.errors);
460
+ violations.push(...r.violations);
461
+ }
462
+ this.throwIfInvalid(errors, violations);
463
+ this.RefusedModules.clear();
464
+ }
465
+ // The registered schemas whose module is at, under or above `path`. None before the boot check, and none while load()
466
+ // rebuilds the tree: it checks the whole tree once it is complete.
467
+ schemasAt(path) {
468
+ if (this.ValidationSchemas === undefined || this.Loading)
469
+ return [];
470
+ const target = lodash_1.default.toPath(path);
471
+ return this.schemasOfLineage().filter((s) => {
472
+ const module = lodash_1.default.toPath(s.$configurationModule);
473
+ const n = Math.min(module.length, target.length);
474
+ return lodash_1.default.isEqual(module.slice(0, n), target.slice(0, n));
475
+ });
476
+ }
477
+ // Runs `change` on a copy of the modules of `schemas` and throws when one of them would become invalid, so that a
478
+ // refused change is never applied.
479
+ checkChange(schemas, path, change) {
480
+ if (schemas.length === 0)
481
+ return;
482
+ const top = lodash_1.default.minBy([lodash_1.default.toPath(path), ...schemas.map((s) => lodash_1.default.toPath(s.$configurationModule))], (p) => p.length) ?? [];
483
+ const root = (top.length === 0 ? copyForCheck(this.Config) : lodash_1.default.set({}, top, copyForCheck(lodash_1.default.get(this.Config, top))));
484
+ change(root);
485
+ const errors = [];
486
+ const violations = [];
487
+ for (const s of schemas) {
488
+ const value = lodash_1.default.get(root, s.$configurationModule);
489
+ if (value === undefined || this.Validator.validate(s, value))
490
+ continue;
491
+ const r = this.failureOf(s);
492
+ errors.push(...r.errors);
493
+ violations.push(...r.violations);
494
+ }
495
+ this.throwIfInvalid(errors, violations);
496
+ }
497
+ // Fills the defaults and coercion of the modules a change that passed checkChange() reached.
498
+ applySchemas(schemas) {
499
+ for (const s of schemas) {
500
+ const r = this.checkModule(s);
501
+ this.throwIfInvalid(r.errors, r.violations);
502
+ this.RefusedModules.delete((0, util_js_1.schemaNameOf)(s));
503
+ }
504
+ }
505
+ /** Validates one module after boot; a refused module stays refused for reads until a later check passes. */
506
+ validateModule(s) {
507
+ const key = (0, util_js_1.schemaNameOf)(s);
508
+ const r = this.checkModule(s);
509
+ try {
510
+ this.throwIfInvalid(r.errors, r.violations);
511
+ }
512
+ catch (e) {
513
+ this.RefusedModules.set(key, { module: lodash_1.default.toPath(s.$configurationModule), error: e });
514
+ throw e;
515
+ }
516
+ this.RefusedModules.delete(key);
517
+ }
518
+ throwIfInvalid(errors, violations) {
519
+ if (violations.length === 0)
520
+ return;
521
+ violations.forEach((v) => internal_logger_1.InternalLogger.error({ path: v.path, schema: v.schema, violation: v.message }, 'Invalid configuration at ${path} ( schema ${schema} ): ${violation}', 'Configuration'));
522
+ throw new exception_js_1.InvalidConfiguration(`Invalid configuration: ${violations.map((v) => `${v.path} ${v.message} ( schema ${v.schema} )`).join('; ')}`, errors, violations);
523
+ }
524
+ checkModule(s) {
525
+ // Not this.get: a refused module is read here to check it again.
526
+ const config = lodash_1.default.get(this.Config, s.$configurationModule);
527
+ if (config === undefined) {
528
+ // An unconfigured module still gets its schema defaults, unless the schema needs values from the app.
529
+ const empty = {};
530
+ if (this.Validator.validate(s, empty)) {
531
+ lodash_1.default.set(this.Config, s.$configurationModule, empty);
290
532
  }
291
- const result = this.Validator.validate(s, config);
292
- if (!result) {
293
- // @ts-ignore
294
- this.Validator.errors.forEach((ve) => {
295
- internal_logger_1.InternalLogger.error('Invalid configuration ! Message: %s, path: %s, keyword: %s, schemaPath: %s, configuration module: %s', 'Configuration', ve.message, ve.instancePath, ve.keyword, ve.schemaPath, s.$configurationModule);
296
- });
297
- throw new exception_js_1.InvalidConfiguration('Validation error', this.Validator.errors);
533
+ else {
534
+ internal_logger_1.InternalLogger.warn({ module: s.$configurationModule }, 'Cannot validate configuration for module ${module}, configuration is not set', 'Configuration');
298
535
  }
299
- });
536
+ return { errors: [], violations: [] };
537
+ }
538
+ // Ajv coerces and fills defaults in place, and evaluates `if` before `properties` coerces. Checking the stored value
539
+ // directly would rewrite a refused value (a null guard's null into 0) so that the next check of it passes; only a
540
+ // value whose copy passes gets its defaults and coercion.
541
+ if (this.Validator.validate(s, copyForCheck(config)) && this.validateInPlace(s, config)) {
542
+ return { errors: [], violations: [] };
543
+ }
544
+ return this.failureOf(s);
545
+ }
546
+ // The violations of the last failed this.Validator.validate(s, ...) call.
547
+ failureOf(s) {
548
+ const errors = this.Validator.errors ?? [];
549
+ const schema = (0, util_js_1.schemaNameOf)(s);
550
+ let violations = toViolations(s.$configurationModule, schema, errors);
551
+ if (violations.length === 0) {
552
+ // An invalid configuration must never pass, whatever shape Ajv reported.
553
+ violations = errors.map((e) => ({ path: [s.$configurationModule, ...e.instancePath.split('/').filter((p) => p !== '').map(decodePointer)].join('.'), schema, message: e.message ?? e.keyword }));
554
+ }
555
+ return { errors, violations };
556
+ }
557
+ validateInPlace(s, config) {
558
+ keepUnresolvedVars = true;
559
+ try {
560
+ return this.Validator.validate(s, config);
561
+ }
562
+ finally {
563
+ keepUnresolvedVars = false;
564
+ }
300
565
  }
301
566
  initValidator() {
302
567
  const options = {
@@ -313,6 +578,8 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
313
578
  useDefaults: true,
314
579
  // The option coerceTypes allows you to have your data types coerced to the types specified in your schema type keywords
315
580
  coerceTypes: true,
581
+ // allow rules across fields, eg. minimum: { $data: '1/min' }
582
+ $data: true,
316
583
  };
317
584
  // @ts-ignore
318
585
  if (ajv_1.default.default) {
@@ -335,11 +602,17 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
335
602
  // we use $configurationModule keyword to identify
336
603
  // to match config module with config schema
337
604
  this.Validator.addKeyword('$configurationModule');
338
- /**
339
- * load all registered configuration json schemas
340
- * for config value validation
341
- */
342
- this.ValidationSchemas = this.Container.get(Array.ofType('__configurationSchema__'), true);
605
+ }
606
+ // A child that registers a schema of its own must not hide the schemas of its ancestors.
607
+ schemasOfLineage() {
608
+ const found = [];
609
+ for (let c = this.Container; c; c = c.Parent) {
610
+ for (const s of (c.Cache.get('__configurationSchema__', false) ?? [])) {
611
+ if (!found.some((f) => (0, util_js_1.sameSchema)(f, s)))
612
+ found.push(s);
613
+ }
614
+ }
615
+ return found;
343
616
  }
344
617
  dir(toJoin) {
345
618
  return (0, path_1.normalize)((0, path_1.join)((0, path_1.resolve)(this.AppBaseDir), toJoin));
@@ -351,8 +624,10 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
351
624
  if (!this.RunApp) {
352
625
  return;
353
626
  }
354
- for (const prop of Object.keys(this.get(['system', 'dirs'], []))) {
355
- this.get(['system', 'dirs', prop]).push(this.dir(`/${this.RunApp}/${prop}`));
627
+ for (const prop of Object.keys(this.get(['system', 'dirs'], {}))) {
628
+ const dirs = this.get(['system', 'dirs', prop]);
629
+ if (Array.isArray(dirs))
630
+ dirs.push(this.dir(`/${this.RunApp}/${prop}`));
356
631
  }
357
632
  }
358
633
  /**
@@ -363,7 +638,7 @@ let FrameworkConfiguration = class FrameworkConfiguration extends configuration_
363
638
  for (const prop of Object.keys(this.Config)) {
364
639
  const subconfig = this.Config[`${prop}`];
365
640
  if (!subconfig) {
366
- internal_logger_1.InternalLogger.warn(`Configuration for ${prop} not exists, check configuration file`, 'configuration');
641
+ internal_logger_1.InternalLogger.warn({ section: prop }, 'Configuration for ${section} not exists, check configuration file', 'configuration');
367
642
  continue;
368
643
  }
369
644
  if (subconfig.configure && typeof subconfig.configure === 'function') {