@spinajs/configuration 2.0.557 → 3.0.1
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/lib/cjs/config/configuration.d.ts +1 -5
- package/lib/cjs/config/configuration.d.ts.map +1 -1
- package/lib/cjs/config/configuration.js +3 -1
- package/lib/cjs/config/configuration.js.map +1 -1
- package/lib/cjs/configuration.d.ts +24 -1
- package/lib/cjs/configuration.d.ts.map +1 -1
- package/lib/cjs/configuration.js +357 -82
- package/lib/cjs/configuration.js.map +1 -1
- package/lib/cjs/decorators.d.ts +77 -4
- package/lib/cjs/decorators.d.ts.map +1 -1
- package/lib/cjs/decorators.js +212 -11
- package/lib/cjs/decorators.js.map +1 -1
- package/lib/cjs/exception.d.ts +9 -1
- package/lib/cjs/exception.d.ts.map +1 -1
- package/lib/cjs/exception.js +2 -1
- package/lib/cjs/exception.js.map +1 -1
- package/lib/cjs/helpers.d.ts.map +1 -1
- package/lib/cjs/helpers.js +3 -2
- package/lib/cjs/helpers.js.map +1 -1
- package/lib/cjs/index.browser.d.ts +1 -0
- package/lib/cjs/index.browser.d.ts.map +1 -1
- package/lib/cjs/index.browser.js +4 -0
- package/lib/cjs/index.browser.js.map +1 -1
- package/lib/cjs/index.d.ts +3 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +6 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/lib-dirs.d.ts +8 -0
- package/lib/cjs/lib-dirs.d.ts.map +1 -0
- package/lib/cjs/lib-dirs.js +16 -0
- package/lib/cjs/lib-dirs.js.map +1 -0
- package/lib/cjs/schemas.d.ts +20 -0
- package/lib/cjs/schemas.d.ts.map +1 -0
- package/lib/cjs/schemas.js +58 -0
- package/lib/cjs/schemas.js.map +1 -0
- package/lib/cjs/sources.d.ts +9 -3
- package/lib/cjs/sources.d.ts.map +1 -1
- package/lib/cjs/sources.js +27 -10
- package/lib/cjs/sources.js.map +1 -1
- package/lib/cjs/util.d.ts +8 -0
- package/lib/cjs/util.d.ts.map +1 -1
- package/lib/cjs/util.js +7 -1
- package/lib/cjs/util.js.map +1 -1
- package/lib/mjs/config/configuration.d.ts +1 -5
- package/lib/mjs/config/configuration.d.ts.map +1 -1
- package/lib/mjs/config/configuration.js +3 -1
- package/lib/mjs/config/configuration.js.map +1 -1
- package/lib/mjs/configuration.d.ts +24 -1
- package/lib/mjs/configuration.d.ts.map +1 -1
- package/lib/mjs/configuration.js +326 -84
- package/lib/mjs/configuration.js.map +1 -1
- package/lib/mjs/decorators.d.ts +77 -4
- package/lib/mjs/decorators.d.ts.map +1 -1
- package/lib/mjs/decorators.js +207 -12
- package/lib/mjs/decorators.js.map +1 -1
- package/lib/mjs/exception.d.ts +9 -1
- package/lib/mjs/exception.d.ts.map +1 -1
- package/lib/mjs/exception.js +2 -1
- package/lib/mjs/exception.js.map +1 -1
- package/lib/mjs/helpers.d.ts.map +1 -1
- package/lib/mjs/helpers.js +3 -2
- package/lib/mjs/helpers.js.map +1 -1
- package/lib/mjs/index.browser.d.ts +1 -0
- package/lib/mjs/index.browser.d.ts.map +1 -1
- package/lib/mjs/index.browser.js +1 -0
- package/lib/mjs/index.browser.js.map +1 -1
- package/lib/mjs/index.d.ts +3 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +3 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/lib-dirs.d.ts +8 -0
- package/lib/mjs/lib-dirs.d.ts.map +1 -0
- package/lib/mjs/lib-dirs.js +13 -0
- package/lib/mjs/lib-dirs.js.map +1 -0
- package/lib/mjs/schemas.d.ts +20 -0
- package/lib/mjs/schemas.d.ts.map +1 -0
- package/lib/mjs/schemas.js +54 -0
- package/lib/mjs/schemas.js.map +1 -0
- package/lib/mjs/sources.d.ts +9 -3
- package/lib/mjs/sources.d.ts.map +1 -1
- package/lib/mjs/sources.js +27 -10
- package/lib/mjs/sources.js.map +1 -1
- package/lib/mjs/util.d.ts +8 -0
- package/lib/mjs/util.d.ts.map +1 -1
- package/lib/mjs/util.js +4 -0
- package/lib/mjs/util.js.map +1 -1
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +7 -7
package/lib/cjs/configuration.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
251
|
+
internal_logger_1.InternalLogger.info({ runApp: this.RunApp }, 'Run app is ${runApp}', 'Configuration');
|
|
118
252
|
}
|
|
119
|
-
internal_logger_1.InternalLogger.info(
|
|
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
|
-
|
|
132
|
-
|
|
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 =
|
|
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:
|
|
312
|
+
APP_ENV: appEnv,
|
|
153
313
|
});
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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])
|
|
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(
|
|
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') {
|