@ti-engine/web-framework 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +4 -4
- package/CHANGELOG.md +384 -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 +660 -663
- package/bin/web-server.js +936 -937
- package/bin/web-server.json +48 -48
- package/components/admin-config-handlers.js +95 -92
- package/components/auth-manager.js +438 -442
- package/components/authorization.js +135 -135
- package/components/config-change-notifier.js +98 -98
- package/components/config-registry.js +257 -260
- package/components/config-service.js +363 -360
- package/components/config-store.js +244 -246
- package/components/definitions.types.js +28 -26
- package/components/session-store.js +113 -110
- package/components/user.js +134 -132
- package/components/web-config-env.js +85 -85
- package/components/web-handlers.js +803 -800
- package/package.json +139 -67
- package/types/bin/web-app-manager.d.ts +194 -0
- package/types/bin/web-server.d.ts +373 -0
- package/types/components/admin-config-handlers.d.ts +11 -0
- package/types/components/auth-manager.d.ts +125 -0
- package/types/components/authorization.d.ts +54 -0
- package/types/components/config-change-notifier.d.ts +73 -0
- package/types/components/config-registry.d.ts +149 -0
- package/types/components/config-service.d.ts +218 -0
- package/types/components/config-store.d.ts +128 -0
- package/types/components/definitions.types.d.ts +31 -0
- package/types/components/session-store.d.ts +56 -0
- package/types/components/user.d.ts +83 -0
- package/types/components/web-config-env.d.ts +17 -0
- package/types/components/web-handlers.d.ts +23 -0
|
@@ -1,360 +1,363 @@
|
|
|
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 exceptions = require( "@ti-engine/core/exceptions" );
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* @
|
|
58
|
-
* @
|
|
59
|
-
* @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return { ok:
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
119
|
-
* @param {
|
|
120
|
-
*
|
|
121
|
-
* @param {
|
|
122
|
-
* @
|
|
123
|
-
* @
|
|
124
|
-
*
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
* @
|
|
178
|
-
* @
|
|
179
|
-
*
|
|
180
|
-
* @
|
|
181
|
-
* @
|
|
182
|
-
* @
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*
|
|
211
|
-
* @
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* @
|
|
216
|
-
* @
|
|
217
|
-
* @
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* @
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
} ) )
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* @
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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 exceptions = require( "@ti-engine/core/exceptions" );
|
|
10
|
+
|
|
11
|
+
/** @import ConfigChangeNotifier from "#config-change-notifier" */
|
|
12
|
+
/** @import ConfigRegistry from "#config-registry" */
|
|
13
|
+
/** @import ConfigStore from "#config-store" */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Orchestrates validated, versioned configuration edits on top of {@link ConfigStore} and {@link ConfigRegistry}.
|
|
17
|
+
*
|
|
18
|
+
* Two layers:
|
|
19
|
+
* - **Document level** — {@link ConfigService#applyEdits}: validate every affected document (schema + semantic,
|
|
20
|
+
* with a cross-document {@link ValidatorContext} whose `getConfig` sees the *pending* values of the same edit —
|
|
21
|
+
* letting a validator check a sibling document's post-edit state — while `getStoredConfig` always returns the
|
|
22
|
+
* committed value, even for the document currently under validation) and, only if all pass, commit them as one
|
|
23
|
+
* change-set. Validation failures return `{ ok:false, errors }` and write nothing; a version conflict from the
|
|
24
|
+
* store surfaces as a rejection.
|
|
25
|
+
* - **Entity level** — composite editors registered with `compose(docs)→view` / `decompose(edited, docs)→{key:value}`,
|
|
26
|
+
* so the UI edits a domain entity (e.g. a "competency") that is projected from, and scattered back into, several
|
|
27
|
+
* documents. {@link ConfigService#saveEditorEdit} decomposes the edit and routes it through `applyEdits`.
|
|
28
|
+
*
|
|
29
|
+
* @class ConfigService
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
class ConfigService {
|
|
33
|
+
|
|
34
|
+
#store;
|
|
35
|
+
#registry;
|
|
36
|
+
#notifier;
|
|
37
|
+
#editors = new Map();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @constructor
|
|
41
|
+
* @param {Object} [options]
|
|
42
|
+
* @param {ConfigStore} [options.store] Defaults to the ConfigStore singleton.
|
|
43
|
+
* @param {ConfigRegistry} [options.registry] Defaults to the ConfigRegistry singleton.
|
|
44
|
+
* @param {ConfigChangeNotifier} [options.notifier] Defaults to the ConfigChangeNotifier singleton.
|
|
45
|
+
*/
|
|
46
|
+
constructor( options = {} ) {
|
|
47
|
+
this.#store = options.store || require( "#config-store" ).instance;
|
|
48
|
+
this.#registry = options.registry || require( "#config-registry" ).instance;
|
|
49
|
+
this.#notifier = options.notifier || require( "#config-change-notifier" ).instance;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Public interface — document level */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates and commits a set of document edits atomically. Each edit: `{ configKey, value, expectedVersion }`.
|
|
56
|
+
*
|
|
57
|
+
* @method
|
|
58
|
+
* @param {Array<{configKey: string, value: Object, expectedVersion: number}>} edits
|
|
59
|
+
* @param {Object} meta
|
|
60
|
+
* @param {string} meta.adminID
|
|
61
|
+
* @param {string} [meta.note]
|
|
62
|
+
* @returns {Promise<{ok: true, changeSetID: string, versions: Object<string, number>} | {ok: false, errors: Object<string, Array>}>}
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
applyEdits( edits, meta ) {
|
|
66
|
+
if ( !Array.isArray( edits ) || edits.length === 0 || !meta || !meta.adminID ) {
|
|
67
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-apply-input" } ) );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Cross-document validation context: a document being edited is seen at its *pending* value via getConfig —
|
|
71
|
+
// even when that document is the one currently under validation, so calling getConfig on "yourself" just
|
|
72
|
+
// hands back the same incoming value already passed as the validator's first argument, not its prior state.
|
|
73
|
+
// This lets a validator on one document check against the post-edit state of its siblings. getStoredConfig
|
|
74
|
+
// is the counterpart: it always resolves the committed value, so a validator that must compare its own
|
|
75
|
+
// document against its previous state (e.g. detecting an edit that should have bumped a version marker)
|
|
76
|
+
// uses that instead.
|
|
77
|
+
const pending = {};
|
|
78
|
+
for ( const edit of edits ) {
|
|
79
|
+
pending[ edit.configKey ] = edit.value;
|
|
80
|
+
}
|
|
81
|
+
const context = {
|
|
82
|
+
getConfig: ( key ) => {
|
|
83
|
+
if ( Object.prototype.hasOwnProperty.call( pending, key ) ) {
|
|
84
|
+
return Promise.resolve( clone( pending[ key ] ) );
|
|
85
|
+
}
|
|
86
|
+
return this.#store.getCurrent( key ).then( ( current ) => ( current ? current.value : null ) );
|
|
87
|
+
},
|
|
88
|
+
// Always the committed value, even for a document inside this edit batch. A validator comparing its own
|
|
89
|
+
// document against its previous state must use this; getConfig would hand back the pending value it is
|
|
90
|
+
// currently validating.
|
|
91
|
+
getStoredConfig: ( key ) => this.#store.getCurrent( key ).then( ( current ) => ( current ? current.value : null ) )
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return Promise.all( edits.map( ( edit ) => {
|
|
95
|
+
return this.#registry.validate( edit.configKey, edit.value, context ).then( ( result ) => ( { configKey: edit.configKey, valid: result.valid, errors: result.errors } ) );
|
|
96
|
+
} ) ).then( ( results ) => {
|
|
97
|
+
const errorsByKey = {};
|
|
98
|
+
for ( const result of results ) {
|
|
99
|
+
if ( !result.valid ) {
|
|
100
|
+
errorsByKey[ result.configKey ] = result.errors;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if ( Object.keys( errorsByKey ).length > 0 ) {
|
|
104
|
+
return { ok: false, errors: errorsByKey };
|
|
105
|
+
}
|
|
106
|
+
return this.#store.saveChangeSet( edits, meta ).then( ( saved ) => {
|
|
107
|
+
this.#notifier.publish( { changeSetID: saved.changeSetID, configKeys: Object.keys( saved.versions ), adminID: meta.adminID, timestamp: new Date().toISOString() } );
|
|
108
|
+
return { ok: true, changeSetID: saved.changeSetID, versions: saved.versions };
|
|
109
|
+
} );
|
|
110
|
+
} );
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Public interface — entity level (composite editors) */
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Registers a composite editor over one or more documents.
|
|
117
|
+
*
|
|
118
|
+
* @method
|
|
119
|
+
* @param {string} editorKey
|
|
120
|
+
* @param {Object} definition
|
|
121
|
+
* @param {string[]} definition.documents The configKeys this editor spans.
|
|
122
|
+
* @param {(configs: Object) => *} definition.compose Maps `{ [configKey]: value }` → a view for the UI.
|
|
123
|
+
* @param {(editedView: *, currentDocs: Object) => Object.<string, Object>} definition.decompose Maps `(editedView, currentDocs)` → the
|
|
124
|
+
* full new values for the documents that changed (`{ [configKey]: newValue }`).
|
|
125
|
+
* @param {Object} [definition.metadata]
|
|
126
|
+
* @returns {ConfigService} this (chainable)
|
|
127
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS}
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
registerEditor( editorKey, definition ) {
|
|
131
|
+
const { documents, compose, decompose, metadata = {} } = definition || {};
|
|
132
|
+
if ( !editorKey || !Array.isArray( documents ) || documents.length === 0 || typeof compose !== "function" || typeof decompose !== "function" ) {
|
|
133
|
+
throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-editor-registration", editorKey: editorKey } );
|
|
134
|
+
}
|
|
135
|
+
this.#editors.set( editorKey, { documents: documents.slice(), compose: compose, decompose: decompose, metadata: metadata || {} } );
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @method
|
|
141
|
+
* @param {string} editorKey
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
* @public
|
|
144
|
+
*/
|
|
145
|
+
hasEditor( editorKey ) {
|
|
146
|
+
return this.#editors.has( editorKey );
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @method
|
|
151
|
+
* @returns {string[]}
|
|
152
|
+
* @public
|
|
153
|
+
*/
|
|
154
|
+
listEditors() {
|
|
155
|
+
return Array.from( this.#editors.keys() );
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Loads the editor's documents and composes them into a view. Returns the view plus the current per-document
|
|
160
|
+
* versions, which the client must echo back on save for optimistic locking.
|
|
161
|
+
*
|
|
162
|
+
* @method
|
|
163
|
+
* @param {string} editorKey
|
|
164
|
+
* @returns {Promise<{rows: *, versions: Object<string, number>}>}
|
|
165
|
+
* @public
|
|
166
|
+
*/
|
|
167
|
+
composeView( editorKey ) {
|
|
168
|
+
const editor = this.#editors.get( editorKey );
|
|
169
|
+
if ( !editor ) {
|
|
170
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-editor", editorKey: editorKey } ) );
|
|
171
|
+
}
|
|
172
|
+
return this.#loadDocuments( editor.documents ).then( ( { docs, versions } ) => ( { rows: editor.compose( clone( docs ) ), versions: versions } ) );
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Applies an edit made against a composite editor: decompose the edited view into per-document new values, then
|
|
177
|
+
* route through {@link ConfigService#applyEdits} (validate-all → atomic change-set). `expectedVersions` should be
|
|
178
|
+
* the versions returned by {@link ConfigService#composeView} when the edit started.
|
|
179
|
+
*
|
|
180
|
+
* @method
|
|
181
|
+
* @param {string} editorKey
|
|
182
|
+
* @param {*} editedView
|
|
183
|
+
* @param {Object} meta
|
|
184
|
+
* @param {Object<string, number>} [expectedVersions]
|
|
185
|
+
* @returns {Promise<Object>} The {@link ConfigService#applyEdits} result (or `{ ok:true, changeSetID:null }` if nothing changed).
|
|
186
|
+
* @public
|
|
187
|
+
*/
|
|
188
|
+
saveEditorEdit( editorKey, editedView, meta, expectedVersions = {} ) {
|
|
189
|
+
const editor = this.#editors.get( editorKey );
|
|
190
|
+
if ( !editor ) {
|
|
191
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-editor", editorKey: editorKey } ) );
|
|
192
|
+
}
|
|
193
|
+
return this.#loadDocuments( editor.documents ).then( ( { docs, versions } ) => {
|
|
194
|
+
const newValues = editor.decompose( editedView, clone( docs ) ) || {};
|
|
195
|
+
const edits = Object.keys( newValues ).map( ( key ) => ( {
|
|
196
|
+
configKey: key,
|
|
197
|
+
value: newValues[ key ],
|
|
198
|
+
expectedVersion: ( expectedVersions && expectedVersions[ key ] != null ) ? expectedVersions[ key ] : versions[ key ]
|
|
199
|
+
} ) );
|
|
200
|
+
if ( edits.length === 0 ) {
|
|
201
|
+
return { ok: true, changeSetID: null, versions: {} };
|
|
202
|
+
}
|
|
203
|
+
return this.applyEdits( edits, meta );
|
|
204
|
+
} );
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Public interface — audit, history, and restore */
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Restores a prior change-set through the validated path: rebuild edits from the change-set's historic snapshots
|
|
211
|
+
* and route them through {@link ConfigService#applyEdits} — so the restore is **re-validated against the current
|
|
212
|
+
* schemas/validators** (a snapshot valid when written may be invalid now) and emits `config:changed`. Returns the
|
|
213
|
+
* `applyEdits` result (`{ ok:false, errors }` if a snapshot no longer validates; nothing is written then).
|
|
214
|
+
*
|
|
215
|
+
* @method
|
|
216
|
+
* @param {string} changeSetID
|
|
217
|
+
* @param {Object} meta
|
|
218
|
+
* @param {string} meta.adminID
|
|
219
|
+
* @param {string} [meta.note]
|
|
220
|
+
* @returns {Promise<Object>}
|
|
221
|
+
* @public
|
|
222
|
+
*/
|
|
223
|
+
restoreChangeSet( changeSetID, meta ) {
|
|
224
|
+
if ( !meta || !meta.adminID ) {
|
|
225
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-restore-input" } ) );
|
|
226
|
+
}
|
|
227
|
+
return this.#store.getChangeSet( changeSetID ).then( ( record ) => {
|
|
228
|
+
if ( !record ) {
|
|
229
|
+
throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-changeset", changeSetID: changeSetID } );
|
|
230
|
+
}
|
|
231
|
+
return Promise.all( record.documents.map( ( doc ) => {
|
|
232
|
+
return Promise.all( [ this.#store.getVersion( doc.configKey, doc.version ), this.#store.getCurrent( doc.configKey ) ] ).then( ( [ historic, current ] ) => ( {
|
|
233
|
+
configKey: doc.configKey,
|
|
234
|
+
value: historic ? historic.snapshot : null,
|
|
235
|
+
expectedVersion: current ? current.version : 0
|
|
236
|
+
} ) );
|
|
237
|
+
} ) ).then( ( edits ) => this.applyEdits( edits, { adminID: meta.adminID, note: meta.note || ( "restored from change-set " + changeSetID ) } ) );
|
|
238
|
+
} );
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @method
|
|
243
|
+
* @param {string} configKey
|
|
244
|
+
* @returns {Promise<Object|null>} The current envelope for a configuration document.
|
|
245
|
+
* @public
|
|
246
|
+
*/
|
|
247
|
+
getCurrent( configKey ) {
|
|
248
|
+
return this.#store.getCurrent( configKey );
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @method
|
|
253
|
+
* @param {string} configKey
|
|
254
|
+
* @returns {Promise<Array<Object>>} The document's version history (ascending), each a full snapshot entry.
|
|
255
|
+
* @public
|
|
256
|
+
*/
|
|
257
|
+
getHistory( configKey ) {
|
|
258
|
+
return this.#store.listHistory( configKey );
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @method
|
|
263
|
+
* @param {string} changeSetID
|
|
264
|
+
* @returns {Promise<Object|null>} A single change-set record.
|
|
265
|
+
* @public
|
|
266
|
+
*/
|
|
267
|
+
getChange( changeSetID ) {
|
|
268
|
+
return this.#store.getChangeSet( changeSetID );
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @method
|
|
273
|
+
* @returns {Promise<Array<Object>>} The cross-document audit feed (change-sets, most-recent first).
|
|
274
|
+
* @public
|
|
275
|
+
*/
|
|
276
|
+
listChanges() {
|
|
277
|
+
return this.#store.listChangeSets();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds a downloadable snapshot of the current live configuration — for every registered document, its repo file
|
|
282
|
+
* `path` (from registration metadata, if provided), current `version`, and `value`. This is the one-way export
|
|
283
|
+
* *out* of the store; an admin downloads it and commits the files to git. The store remains the live truth.
|
|
284
|
+
*
|
|
285
|
+
* @method
|
|
286
|
+
* @param {Object} [meta]
|
|
287
|
+
* @param {string} [meta.adminID]
|
|
288
|
+
* @returns {Promise<{exportedAt: string, exportedBy: (string|null), documents: Array<{configKey: string, path: (string|null), version: number, value: Object}>}>}
|
|
289
|
+
* @public
|
|
290
|
+
*/
|
|
291
|
+
exportBundle( meta = {} ) {
|
|
292
|
+
const keys = this.#registry.list();
|
|
293
|
+
return Promise.all( keys.map( ( configKey ) => {
|
|
294
|
+
return this.#store.getCurrent( configKey ).then( ( current ) => {
|
|
295
|
+
const metadata = this.#registry.metadataFor( configKey ) || {};
|
|
296
|
+
return {
|
|
297
|
+
configKey: configKey,
|
|
298
|
+
path: metadata.path || null,
|
|
299
|
+
version: current ? current.version : 0,
|
|
300
|
+
value: current ? current.value : null
|
|
301
|
+
};
|
|
302
|
+
} );
|
|
303
|
+
} ) ).then( ( documents ) => ( {
|
|
304
|
+
exportedAt: new Date().toISOString(),
|
|
305
|
+
exportedBy: meta.adminID || null,
|
|
306
|
+
documents: documents
|
|
307
|
+
} ) );
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
|
|
312
|
+
* Used by an application to bring its file defaults into the store at startup before serving live config.
|
|
313
|
+
*
|
|
314
|
+
* @method
|
|
315
|
+
* @param {string} configKey
|
|
316
|
+
* @param {Object} defaultValue
|
|
317
|
+
* @returns {Promise<Object>} The current envelope.
|
|
318
|
+
* @public
|
|
319
|
+
*/
|
|
320
|
+
seedDefault( configKey, defaultValue ) {
|
|
321
|
+
return this.#store.seedIfEmpty( configKey, defaultValue );
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Subscribes a listener to `config:changed` events (delegates to the change notifier). Returns an unsubscribe fn.
|
|
326
|
+
*
|
|
327
|
+
* @method
|
|
328
|
+
* @param {(configs: Object) => void} listener
|
|
329
|
+
* @returns {() => void}
|
|
330
|
+
* @public
|
|
331
|
+
*/
|
|
332
|
+
onConfigChanged( listener ) {
|
|
333
|
+
return this.#notifier.subscribe( listener );
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Private interface */
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @method
|
|
340
|
+
* @param {string[]} keys
|
|
341
|
+
* @returns {Promise<{docs: Object<string, Object>, versions: Object<string, number>}>}
|
|
342
|
+
*/
|
|
343
|
+
#loadDocuments( keys ) {
|
|
344
|
+
return Promise.all( keys.map( ( key ) => this.#store.getCurrent( key ) ) ).then( ( currents ) => {
|
|
345
|
+
const docs = {};
|
|
346
|
+
const versions = {};
|
|
347
|
+
keys.forEach( ( key, index ) => {
|
|
348
|
+
docs[ key ] = currents[ index ] ? currents[ index ].value : null;
|
|
349
|
+
versions[ key ] = currents[ index ] ? currents[ index ].version : 0;
|
|
350
|
+
} );
|
|
351
|
+
return { docs: docs, versions: versions };
|
|
352
|
+
} );
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clone( value ) {
|
|
358
|
+
return value === undefined || value === null ? value : JSON.parse( JSON.stringify( value ) );
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const instance = new ConfigService();
|
|
362
|
+
module.exports = ConfigService;
|
|
363
|
+
ConfigService.instance = instance;
|