@ti-engine/web-framework 1.19.0 → 1.19.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/.env +4 -4
- package/CHANGELOG.md +362 -353
- package/README.md +73 -73
- package/bin/build/post-install.js +18 -18
- package/bin/localization/web-server-labels.json +27 -27
- package/bin/static/.well-known/appspecific/com.chrome.devtools.json +5 -5
- package/bin/static/fragments/components/component-notification-bar.html +21 -21
- package/bin/static/fragments/components/component-sidebar.html +33 -33
- package/bin/static/fragments/components/component-tooltip.html +10 -10
- package/bin/static/fragments/components/component-topbar.html +5 -5
- package/bin/static/fragments/frame-administration.html +2 -2
- package/bin/static/fragments/frame-application.html +18 -18
- package/bin/static/fragments/frame-dashboard.html +2 -2
- package/bin/static/fragments/frame-login.html +119 -119
- package/bin/static/fragments/frame-not-found.html +2 -2
- package/bin/static/fragments/frame-profile.html +2 -2
- package/bin/static/index.html +22 -22
- package/bin/static/scripts/ti-charts.js +1591 -1591
- package/bin/static/scripts/ti-framework.css +3194 -3194
- package/bin/static/scripts/ti-framework.js +1427 -1427
- package/bin/static/scripts/ti-theme-black-glass.css +216 -216
- package/bin/static/scripts/ti-theme-daylight.css +87 -87
- package/bin/web-app-manager.js +663 -663
- package/bin/web-server.js +936 -936
- package/bin/web-server.json +48 -48
- package/components/admin-config-handlers.js +92 -92
- package/components/auth-manager.js +441 -441
- package/components/authorization.js +135 -135
- package/components/config-change-notifier.js +98 -98
- package/components/config-registry.js +260 -260
- package/components/config-service.js +360 -360
- package/components/config-store.js +246 -246
- package/components/definitions.types.js +26 -26
- package/components/session-store.js +110 -110
- package/components/user.js +132 -132
- package/components/web-config-env.js +85 -85
- package/components/web-handlers.js +800 -800
- package/package.json +76 -67
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
-
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
-
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
-
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const Ajv = require( "ajv" );
|
|
10
|
-
const exceptions = require( "@ti-engine/core/exceptions" );
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Converts an ajv `instancePath` (a JSON Pointer, e.g. `/competencies/E1-1/name`) into the dot/bracket data path this
|
|
14
|
-
* registry has always exposed on schema issues (e.g. `.competencies.E1-1.name`, array indices rendered as `[0]`).
|
|
15
|
-
* ajv 8 renamed `dataPath` (dot style) to `instancePath` (JSON Pointer); normalizing here keeps the public
|
|
16
|
-
* {@link ConfigValidationIssue} `path` contract stable across the ajv 6 → 8 upgrade.
|
|
17
|
-
*
|
|
18
|
-
* @param {string} [instancePath] The ajv instance path (JSON Pointer), or empty for the document root.
|
|
19
|
-
* @returns {string} The dot/bracket data path, or "" for the document root.
|
|
20
|
-
*/
|
|
21
|
-
const instancePathToDataPath = ( instancePath ) => {
|
|
22
|
-
if ( !instancePath ) {
|
|
23
|
-
return "";
|
|
24
|
-
}
|
|
25
|
-
return instancePath
|
|
26
|
-
.split( "/" )
|
|
27
|
-
.slice( 1 )
|
|
28
|
-
.map( ( segment ) => {
|
|
29
|
-
const key = segment.replace( /~1/g, "/" ).replace( /~0/g, "~" );
|
|
30
|
-
return ( /^\d+$/.test( key ) ) ? `[${ key }]` : `.${ key }`;
|
|
31
|
-
} )
|
|
32
|
-
.join( "" );
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @typedef {Object} ConfigValidationIssue
|
|
37
|
-
* @property {string} path A JSON pointer / data path to the offending value (e.g. ".competencies.E1-1.name"), or "".
|
|
38
|
-
* @property {string} message Human-readable problem description.
|
|
39
|
-
* @property {string} code "schema" for JSON-Schema failures, or a validator-supplied code (default "semantic").
|
|
40
|
-
* @property {Object} [params] Optional structured details (e.g. ajv params).
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The cross-document read context passed to every {@link SemanticValidator}, built fresh for each
|
|
45
|
-
* {@link ConfigService#applyEdits} call.
|
|
46
|
-
*
|
|
47
|
-
* @typedef {Object} ValidatorContext
|
|
48
|
-
* @property {function(string): Promise<*>} getConfig Resolves the *pending* value of `key` when it is part of the
|
|
49
|
-
* current edit batch, otherwise its current committed value. Lets a validator check a sibling document's
|
|
50
|
-
* post-edit state — but calling this for the document being validated itself just returns the same incoming
|
|
51
|
-
* value already passed as the validator's first argument, not its prior state.
|
|
52
|
-
* @property {function(string): Promise<*>} getStoredConfig Always resolves the current committed value of `key`,
|
|
53
|
-
* even when `key` is the document currently under validation. Use this to compare a document against its own
|
|
54
|
-
* previous state (e.g. detecting an edit that should have bumped a version marker).
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @typedef {
|
|
59
|
-
* A semantic validator receives the candidate value and a {@link ValidatorContext} and returns the issues it found
|
|
60
|
-
* (empty array = OK). May be async.
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Registry of editable configuration *documents* and the gate that validates a candidate value against a
|
|
65
|
-
* document's JSON Schema (ajv) plus its semantic validators. The framework stays domain-agnostic: an application
|
|
66
|
-
* registers its config documents (schemas, validators, defaults, editor metadata) at startup; this component knows
|
|
67
|
-
* only "validated, versioned JSON documents". Validation must pass *before* {@link ConfigStore#saveChangeSet}.
|
|
68
|
-
*
|
|
69
|
-
* @class ConfigRegistry
|
|
70
|
-
* @public
|
|
71
|
-
*/
|
|
72
|
-
class ConfigRegistry {
|
|
73
|
-
|
|
74
|
-
#ajv;
|
|
75
|
-
#registrations = new Map();
|
|
76
|
-
|
|
77
|
-
constructor() {
|
|
78
|
-
// ajv 8 speaks Draft-07 by default; our schema files annotate Draft 2020-12 only for editor support, so we
|
|
79
|
-
// strip that annotation (see #stripUnsupportedMeta) and skip meta-validation.
|
|
80
|
-
this.#ajv = new Ajv( { allErrors: true, schemaId: "$id", meta: true, validateSchema: false } );
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* Public interface */
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Registers an editable configuration document.
|
|
87
|
-
*
|
|
88
|
-
* @method
|
|
89
|
-
* @param {string} configKey Stable identifier for the document (also the ConfigStore key).
|
|
90
|
-
* @param {Object} definition
|
|
91
|
-
* @param {Object} definition.schema A JSON Schema for the document.
|
|
92
|
-
* @param {SemanticValidator[]} [definition.validators] Cross-cutting/semantic checks beyond the schema.
|
|
93
|
-
* @param {Object} [definition.defaultValue] The bootstrap default (seeds an empty store).
|
|
94
|
-
* @param {Object} [definition.metadata] Editor metadata (label, editor type, group, risk class, …).
|
|
95
|
-
* @returns {ConfigRegistry} this (chainable)
|
|
96
|
-
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On missing key/schema.
|
|
97
|
-
* @public
|
|
98
|
-
*/
|
|
99
|
-
register( configKey, definition ) {
|
|
100
|
-
const { schema, validators = [], defaultValue, metadata = {} } = definition || {};
|
|
101
|
-
if ( !configKey || !schema || typeof schema !== "object" ) {
|
|
102
|
-
throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-config-registration", configKey: configKey } );
|
|
103
|
-
}
|
|
104
|
-
const prepared = this.#stripUnsupportedMeta( schema );
|
|
105
|
-
this.addSchema( prepared );
|
|
106
|
-
this.#registrations.set( configKey, {
|
|
107
|
-
schema: prepared,
|
|
108
|
-
validators: Array.isArray( validators ) ? validators : [],
|
|
109
|
-
defaultValue: defaultValue,
|
|
110
|
-
metadata: metadata || {},
|
|
111
|
-
compiled: null
|
|
112
|
-
} );
|
|
113
|
-
return this;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Adds a schema that is referenced (via `$ref`/`$id`) by document schemas but is not itself a document.
|
|
118
|
-
*
|
|
119
|
-
* @method
|
|
120
|
-
* @param {Object} schema
|
|
121
|
-
* @returns {ConfigRegistry} this (chainable)
|
|
122
|
-
* @public
|
|
123
|
-
*/
|
|
124
|
-
addSchema( schema ) {
|
|
125
|
-
const prepared = this.#stripUnsupportedMeta( schema );
|
|
126
|
-
if ( prepared && prepared.$id && !this.#ajv.getSchema( prepared.$id ) ) {
|
|
127
|
-
this.#ajv.addSchema( prepared );
|
|
128
|
-
}
|
|
129
|
-
return this;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @method
|
|
134
|
-
* @param {string} configKey
|
|
135
|
-
* @returns {boolean}
|
|
136
|
-
* @public
|
|
137
|
-
*/
|
|
138
|
-
has( configKey ) {
|
|
139
|
-
return this.#registrations.has( configKey );
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* @method
|
|
144
|
-
* @returns {string[]} All registered configuration keys.
|
|
145
|
-
* @public
|
|
146
|
-
*/
|
|
147
|
-
list() {
|
|
148
|
-
return Array.from( this.#registrations.keys() );
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @method
|
|
153
|
-
* @param {string} configKey
|
|
154
|
-
* @returns {Object|undefined} The editor metadata registered for the document.
|
|
155
|
-
* @public
|
|
156
|
-
*/
|
|
157
|
-
metadataFor( configKey ) {
|
|
158
|
-
const registration = this.#registrations.get( configKey );
|
|
159
|
-
return registration ? registration.metadata : undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* @method
|
|
164
|
-
* @param {string} configKey
|
|
165
|
-
* @returns {Object|undefined} The bootstrap default value registered for the document.
|
|
166
|
-
* @public
|
|
167
|
-
*/
|
|
168
|
-
getDefault( configKey ) {
|
|
169
|
-
const registration = this.#registrations.get( configKey );
|
|
170
|
-
return registration ? registration.defaultValue : undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Validates a candidate value for a registered document: JSON Schema first, then the semantic validators.
|
|
175
|
-
* Resolves with `{ valid, errors }` (errors is an array of {@link ConfigValidationIssue}).
|
|
176
|
-
*
|
|
177
|
-
* @method
|
|
178
|
-
* @param {string} configKey
|
|
179
|
-
* @param {Object} value
|
|
180
|
-
* @param {ValidatorContext} [context] Passed to each semantic validator.
|
|
181
|
-
* @returns {Promise<{valid: boolean, errors: ConfigValidationIssue[]}>}
|
|
182
|
-
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
183
|
-
* @public
|
|
184
|
-
*/
|
|
185
|
-
validate( configKey, value, context = {} ) {
|
|
186
|
-
const registration = this.#registrations.get( configKey );
|
|
187
|
-
if ( !registration ) {
|
|
188
|
-
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKey: configKey } ) );
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const errors = [];
|
|
192
|
-
const validateSchema = this.#schemaValidator( registration );
|
|
193
|
-
if ( !validateSchema( value ) ) {
|
|
194
|
-
for ( const error of ( validateSchema.errors || [] ) ) {
|
|
195
|
-
errors.push( { path: error.dataPath || instancePathToDataPath( error.instancePath ), message: error.message || "schema violation", code: "schema", params: error.params } );
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return Promise.all( registration.validators.map( ( validator ) => {
|
|
200
|
-
return Promise.resolve( validator( value, context ) ).then( ( issues ) => ( Array.isArray( issues ) ? issues : [] ) );
|
|
201
|
-
} ) ).then( ( results ) => {
|
|
202
|
-
for ( const issues of results ) {
|
|
203
|
-
for ( const issue of issues ) {
|
|
204
|
-
errors.push( this.#normalizeIssue( issue ) );
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return { valid: errors.length === 0, errors: errors };
|
|
208
|
-
} );
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* Private interface */
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* @method
|
|
215
|
-
* @param {Object} registration
|
|
216
|
-
* @returns {Function} The compiled (and cached) ajv validate function for the document's schema.
|
|
217
|
-
* @private
|
|
218
|
-
*/
|
|
219
|
-
#schemaValidator( registration ) {
|
|
220
|
-
if ( !registration.compiled ) {
|
|
221
|
-
registration.compiled = registration.schema.$id
|
|
222
|
-
? this.#ajv.getSchema( registration.schema.$id )
|
|
223
|
-
: this.#ajv.compile( registration.schema );
|
|
224
|
-
}
|
|
225
|
-
return registration.compiled;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @method
|
|
230
|
-
* @param {Object} schema
|
|
231
|
-
* @returns {Object} A shallow copy without the ajv-6-incompatible Draft-2020-12 `$schema` annotation.
|
|
232
|
-
* @private
|
|
233
|
-
*/
|
|
234
|
-
#stripUnsupportedMeta( schema ) {
|
|
235
|
-
if ( schema && schema.$schema && String( schema.$schema ).includes( "draft/2020-12" ) ) {
|
|
236
|
-
const clone = { ...schema };
|
|
237
|
-
delete clone.$schema;
|
|
238
|
-
return clone;
|
|
239
|
-
}
|
|
240
|
-
return schema;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* @method
|
|
245
|
-
* @param {ConfigValidationIssue|string} issue
|
|
246
|
-
* @returns {ConfigValidationIssue}
|
|
247
|
-
* @private
|
|
248
|
-
*/
|
|
249
|
-
#normalizeIssue( issue ) {
|
|
250
|
-
if ( typeof issue === "string" ) {
|
|
251
|
-
return { path: "", message: issue, code: "semantic" };
|
|
252
|
-
}
|
|
253
|
-
return { path: issue.path || "", message: issue.message || "validation failed", code: issue.code || "semantic", params: issue.params };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const instance = new ConfigRegistry();
|
|
259
|
-
module.exports = ConfigRegistry;
|
|
260
|
-
module.exports.instance = instance;
|
|
1
|
+
/*
|
|
2
|
+
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
+
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
+
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const Ajv = require( "ajv" );
|
|
10
|
+
const exceptions = require( "@ti-engine/core/exceptions" );
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts an ajv `instancePath` (a JSON Pointer, e.g. `/competencies/E1-1/name`) into the dot/bracket data path this
|
|
14
|
+
* registry has always exposed on schema issues (e.g. `.competencies.E1-1.name`, array indices rendered as `[0]`).
|
|
15
|
+
* ajv 8 renamed `dataPath` (dot style) to `instancePath` (JSON Pointer); normalizing here keeps the public
|
|
16
|
+
* {@link ConfigValidationIssue} `path` contract stable across the ajv 6 → 8 upgrade.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} [instancePath] The ajv instance path (JSON Pointer), or empty for the document root.
|
|
19
|
+
* @returns {string} The dot/bracket data path, or "" for the document root.
|
|
20
|
+
*/
|
|
21
|
+
const instancePathToDataPath = ( instancePath ) => {
|
|
22
|
+
if ( !instancePath ) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return instancePath
|
|
26
|
+
.split( "/" )
|
|
27
|
+
.slice( 1 )
|
|
28
|
+
.map( ( segment ) => {
|
|
29
|
+
const key = segment.replace( /~1/g, "/" ).replace( /~0/g, "~" );
|
|
30
|
+
return ( /^\d+$/.test( key ) ) ? `[${ key }]` : `.${ key }`;
|
|
31
|
+
} )
|
|
32
|
+
.join( "" );
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} ConfigValidationIssue
|
|
37
|
+
* @property {string} path A JSON pointer / data path to the offending value (e.g. ".competencies.E1-1.name"), or "".
|
|
38
|
+
* @property {string} message Human-readable problem description.
|
|
39
|
+
* @property {string} code "schema" for JSON-Schema failures, or a validator-supplied code (default "semantic").
|
|
40
|
+
* @property {Object} [params] Optional structured details (e.g. ajv params).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The cross-document read context passed to every {@link SemanticValidator}, built fresh for each
|
|
45
|
+
* {@link ConfigService#applyEdits} call.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {Object} ValidatorContext
|
|
48
|
+
* @property {function(string): Promise<*>} getConfig Resolves the *pending* value of `key` when it is part of the
|
|
49
|
+
* current edit batch, otherwise its current committed value. Lets a validator check a sibling document's
|
|
50
|
+
* post-edit state — but calling this for the document being validated itself just returns the same incoming
|
|
51
|
+
* value already passed as the validator's first argument, not its prior state.
|
|
52
|
+
* @property {function(string): Promise<*>} getStoredConfig Always resolves the current committed value of `key`,
|
|
53
|
+
* even when `key` is the document currently under validation. Use this to compare a document against its own
|
|
54
|
+
* previous state (e.g. detecting an edit that should have bumped a version marker).
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {(value: Object, context: ValidatorContext) => ConfigValidationIssue[]|Promise<ConfigValidationIssue[]>} SemanticValidator
|
|
59
|
+
* A semantic validator receives the candidate value and a {@link ValidatorContext} and returns the issues it found
|
|
60
|
+
* (empty array = OK). May be async.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Registry of editable configuration *documents* and the gate that validates a candidate value against a
|
|
65
|
+
* document's JSON Schema (ajv) plus its semantic validators. The framework stays domain-agnostic: an application
|
|
66
|
+
* registers its config documents (schemas, validators, defaults, editor metadata) at startup; this component knows
|
|
67
|
+
* only "validated, versioned JSON documents". Validation must pass *before* {@link ConfigStore#saveChangeSet}.
|
|
68
|
+
*
|
|
69
|
+
* @class ConfigRegistry
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
class ConfigRegistry {
|
|
73
|
+
|
|
74
|
+
#ajv;
|
|
75
|
+
#registrations = new Map();
|
|
76
|
+
|
|
77
|
+
constructor() {
|
|
78
|
+
// ajv 8 speaks Draft-07 by default; our schema files annotate Draft 2020-12 only for editor support, so we
|
|
79
|
+
// strip that annotation (see #stripUnsupportedMeta) and skip meta-validation.
|
|
80
|
+
this.#ajv = new Ajv( { allErrors: true, schemaId: "$id", meta: true, validateSchema: false } );
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Public interface */
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Registers an editable configuration document.
|
|
87
|
+
*
|
|
88
|
+
* @method
|
|
89
|
+
* @param {string} configKey Stable identifier for the document (also the ConfigStore key).
|
|
90
|
+
* @param {Object} definition
|
|
91
|
+
* @param {Object} definition.schema A JSON Schema for the document.
|
|
92
|
+
* @param {SemanticValidator[]} [definition.validators] Cross-cutting/semantic checks beyond the schema.
|
|
93
|
+
* @param {Object} [definition.defaultValue] The bootstrap default (seeds an empty store).
|
|
94
|
+
* @param {Object} [definition.metadata] Editor metadata (label, editor type, group, risk class, …).
|
|
95
|
+
* @returns {ConfigRegistry} this (chainable)
|
|
96
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On missing key/schema.
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
register( configKey, definition ) {
|
|
100
|
+
const { schema, validators = [], defaultValue, metadata = {} } = definition || {};
|
|
101
|
+
if ( !configKey || !schema || typeof schema !== "object" ) {
|
|
102
|
+
throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-config-registration", configKey: configKey } );
|
|
103
|
+
}
|
|
104
|
+
const prepared = this.#stripUnsupportedMeta( schema );
|
|
105
|
+
this.addSchema( prepared );
|
|
106
|
+
this.#registrations.set( configKey, {
|
|
107
|
+
schema: prepared,
|
|
108
|
+
validators: Array.isArray( validators ) ? validators : [],
|
|
109
|
+
defaultValue: defaultValue,
|
|
110
|
+
metadata: metadata || {},
|
|
111
|
+
compiled: null
|
|
112
|
+
} );
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Adds a schema that is referenced (via `$ref`/`$id`) by document schemas but is not itself a document.
|
|
118
|
+
*
|
|
119
|
+
* @method
|
|
120
|
+
* @param {Object} schema
|
|
121
|
+
* @returns {ConfigRegistry} this (chainable)
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
addSchema( schema ) {
|
|
125
|
+
const prepared = this.#stripUnsupportedMeta( schema );
|
|
126
|
+
if ( prepared && prepared.$id && !this.#ajv.getSchema( prepared.$id ) ) {
|
|
127
|
+
this.#ajv.addSchema( prepared );
|
|
128
|
+
}
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @method
|
|
134
|
+
* @param {string} configKey
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
* @public
|
|
137
|
+
*/
|
|
138
|
+
has( configKey ) {
|
|
139
|
+
return this.#registrations.has( configKey );
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @method
|
|
144
|
+
* @returns {string[]} All registered configuration keys.
|
|
145
|
+
* @public
|
|
146
|
+
*/
|
|
147
|
+
list() {
|
|
148
|
+
return Array.from( this.#registrations.keys() );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @method
|
|
153
|
+
* @param {string} configKey
|
|
154
|
+
* @returns {Object|undefined} The editor metadata registered for the document.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
metadataFor( configKey ) {
|
|
158
|
+
const registration = this.#registrations.get( configKey );
|
|
159
|
+
return registration ? registration.metadata : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @method
|
|
164
|
+
* @param {string} configKey
|
|
165
|
+
* @returns {Object|undefined} The bootstrap default value registered for the document.
|
|
166
|
+
* @public
|
|
167
|
+
*/
|
|
168
|
+
getDefault( configKey ) {
|
|
169
|
+
const registration = this.#registrations.get( configKey );
|
|
170
|
+
return registration ? registration.defaultValue : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validates a candidate value for a registered document: JSON Schema first, then the semantic validators.
|
|
175
|
+
* Resolves with `{ valid, errors }` (errors is an array of {@link ConfigValidationIssue}).
|
|
176
|
+
*
|
|
177
|
+
* @method
|
|
178
|
+
* @param {string} configKey
|
|
179
|
+
* @param {Object} value
|
|
180
|
+
* @param {ValidatorContext} [context] Passed to each semantic validator.
|
|
181
|
+
* @returns {Promise<{valid: boolean, errors: ConfigValidationIssue[]}>}
|
|
182
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
validate( configKey, value, context = {} ) {
|
|
186
|
+
const registration = this.#registrations.get( configKey );
|
|
187
|
+
if ( !registration ) {
|
|
188
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKey: configKey } ) );
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const errors = [];
|
|
192
|
+
const validateSchema = this.#schemaValidator( registration );
|
|
193
|
+
if ( !validateSchema( value ) ) {
|
|
194
|
+
for ( const error of ( validateSchema.errors || [] ) ) {
|
|
195
|
+
errors.push( { path: error.dataPath || instancePathToDataPath( error.instancePath ), message: error.message || "schema violation", code: "schema", params: error.params } );
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Promise.all( registration.validators.map( ( validator ) => {
|
|
200
|
+
return Promise.resolve( validator( value, context ) ).then( ( issues ) => ( Array.isArray( issues ) ? issues : [] ) );
|
|
201
|
+
} ) ).then( ( results ) => {
|
|
202
|
+
for ( const issues of results ) {
|
|
203
|
+
for ( const issue of issues ) {
|
|
204
|
+
errors.push( this.#normalizeIssue( issue ) );
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { valid: errors.length === 0, errors: errors };
|
|
208
|
+
} );
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Private interface */
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @method
|
|
215
|
+
* @param {Object} registration
|
|
216
|
+
* @returns {Function} The compiled (and cached) ajv validate function for the document's schema.
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
#schemaValidator( registration ) {
|
|
220
|
+
if ( !registration.compiled ) {
|
|
221
|
+
registration.compiled = registration.schema.$id
|
|
222
|
+
? this.#ajv.getSchema( registration.schema.$id )
|
|
223
|
+
: this.#ajv.compile( registration.schema );
|
|
224
|
+
}
|
|
225
|
+
return registration.compiled;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @method
|
|
230
|
+
* @param {Object} schema
|
|
231
|
+
* @returns {Object} A shallow copy without the ajv-6-incompatible Draft-2020-12 `$schema` annotation.
|
|
232
|
+
* @private
|
|
233
|
+
*/
|
|
234
|
+
#stripUnsupportedMeta( schema ) {
|
|
235
|
+
if ( schema && schema.$schema && String( schema.$schema ).includes( "draft/2020-12" ) ) {
|
|
236
|
+
const clone = { ...schema };
|
|
237
|
+
delete clone.$schema;
|
|
238
|
+
return clone;
|
|
239
|
+
}
|
|
240
|
+
return schema;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @method
|
|
245
|
+
* @param {ConfigValidationIssue|string} issue
|
|
246
|
+
* @returns {ConfigValidationIssue}
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
#normalizeIssue( issue ) {
|
|
250
|
+
if ( typeof issue === "string" ) {
|
|
251
|
+
return { path: "", message: issue, code: "semantic" };
|
|
252
|
+
}
|
|
253
|
+
return { path: issue.path || "", message: issue.message || "validation failed", code: issue.code || "semantic", params: issue.params };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const instance = new ConfigRegistry();
|
|
259
|
+
module.exports = ConfigRegistry;
|
|
260
|
+
module.exports.instance = instance;
|