@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.
Files changed (52) hide show
  1. package/.env +4 -4
  2. package/CHANGELOG.md +384 -353
  3. package/README.md +73 -73
  4. package/bin/build/post-install.js +18 -18
  5. package/bin/localization/web-server-labels.json +27 -27
  6. package/bin/static/.well-known/appspecific/com.chrome.devtools.json +5 -5
  7. package/bin/static/fragments/components/component-notification-bar.html +21 -21
  8. package/bin/static/fragments/components/component-sidebar.html +33 -33
  9. package/bin/static/fragments/components/component-tooltip.html +10 -10
  10. package/bin/static/fragments/components/component-topbar.html +5 -5
  11. package/bin/static/fragments/frame-administration.html +2 -2
  12. package/bin/static/fragments/frame-application.html +18 -18
  13. package/bin/static/fragments/frame-dashboard.html +2 -2
  14. package/bin/static/fragments/frame-login.html +119 -119
  15. package/bin/static/fragments/frame-not-found.html +2 -2
  16. package/bin/static/fragments/frame-profile.html +2 -2
  17. package/bin/static/index.html +22 -22
  18. package/bin/static/scripts/ti-charts.js +1591 -1591
  19. package/bin/static/scripts/ti-framework.css +3194 -3194
  20. package/bin/static/scripts/ti-framework.js +1427 -1427
  21. package/bin/static/scripts/ti-theme-black-glass.css +216 -216
  22. package/bin/static/scripts/ti-theme-daylight.css +87 -87
  23. package/bin/web-app-manager.js +660 -663
  24. package/bin/web-server.js +936 -937
  25. package/bin/web-server.json +48 -48
  26. package/components/admin-config-handlers.js +95 -92
  27. package/components/auth-manager.js +438 -442
  28. package/components/authorization.js +135 -135
  29. package/components/config-change-notifier.js +98 -98
  30. package/components/config-registry.js +257 -260
  31. package/components/config-service.js +363 -360
  32. package/components/config-store.js +244 -246
  33. package/components/definitions.types.js +28 -26
  34. package/components/session-store.js +113 -110
  35. package/components/user.js +134 -132
  36. package/components/web-config-env.js +85 -85
  37. package/components/web-handlers.js +803 -800
  38. package/package.json +139 -67
  39. package/types/bin/web-app-manager.d.ts +194 -0
  40. package/types/bin/web-server.d.ts +373 -0
  41. package/types/components/admin-config-handlers.d.ts +11 -0
  42. package/types/components/auth-manager.d.ts +125 -0
  43. package/types/components/authorization.d.ts +54 -0
  44. package/types/components/config-change-notifier.d.ts +73 -0
  45. package/types/components/config-registry.d.ts +149 -0
  46. package/types/components/config-service.d.ts +218 -0
  47. package/types/components/config-store.d.ts +128 -0
  48. package/types/components/definitions.types.d.ts +31 -0
  49. package/types/components/session-store.d.ts +56 -0
  50. package/types/components/user.d.ts +83 -0
  51. package/types/components/web-config-env.d.ts +17 -0
  52. package/types/components/web-handlers.d.ts +23 -0
@@ -1,246 +1,244 @@
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 cache = require( "@ti-engine/core/cache" );
10
- const tools = require( "@ti-engine/core/tools" );
11
- const exceptions = require( "@ti-engine/core/exceptions" );
12
-
13
- const KEY_CURRENT = "ti:config:cur:"; // + configKey → current document envelope
14
- const KEY_HISTORY = "ti:config:hist:"; // + configKey + ":" + version → history snapshot entry
15
- const KEY_CHANGESET = "ti:config:cs:"; // + changeSetID → change-set record
16
- const SEED_ACTOR = "system:seed";
17
-
18
- /**
19
- * A versioned, change-set-aware configuration store backed by the common memory cache (RedisJSON).
20
- * <br/>
21
- * Each editable configuration is a *document* identified by a `configKey`. Every committed edit:
22
- * - bumps the document's monotonic `version`,
23
- * - writes a full **snapshot** to history (enabling restore),
24
- * - and is correlated with the other documents written in the same logical edit via a shared **change-set** id,
25
- * so a multi-document edit (and its restore) is treated as one unit even though storage is per-document.
26
- * <br/>
27
- * Optimistic locking: callers pass the `expectedVersion` they edited from; the save is rejected if any document
28
- * moved on in the meantime. This component is storage-only — schema/semantic validation is a separate pipeline
29
- * that must run *before* {@link ConfigStore#saveChangeSet}.
30
- * <br/>
31
- * NOTE: true cross-document atomicity is not provided (the cache exposes per-key commands only). All locks are
32
- * checked *before* any write, so the common conflict case is safe; a mid-write process failure can leave a
33
- * partially-applied change-set, detectable via the change-set record. Hardening (a Lua/MULTI write) is deferred.
34
- *
35
- * @class ConfigStore
36
- * @public
37
- */
38
- class ConfigStore {
39
-
40
- /* Public interface */
41
-
42
- /**
43
- * Returns the current envelope `{ value, version, updatedAt, updatedBy, changeSetID }` for a configuration
44
- * document, or `null` if it has never been written.
45
- *
46
- * @method
47
- * @param {string} configKey
48
- * @returns {Promise<Object|null>}
49
- * @public
50
- */
51
- getCurrent( configKey ) {
52
- return this.#readJSON( KEY_CURRENT + configKey );
53
- }
54
-
55
- /**
56
- * Writes the default value as version 1 only if the document does not yet exist (idempotent bootstrap).
57
- * Resolves with the current envelope either way.
58
- *
59
- * @method
60
- * @param {string} configKey
61
- * @param {Object} defaultValue
62
- * @returns {Promise<Object>}
63
- * @public
64
- */
65
- seedIfEmpty( configKey, defaultValue ) {
66
- return this.getCurrent( configKey ).then( ( current ) => {
67
- if ( current ) return current;
68
- const timestamp = new Date().toISOString();
69
- const envelope = { value: defaultValue, version: 1, updatedAt: timestamp, updatedBy: SEED_ACTOR, changeSetID: null };
70
- const historyEntry = { version: 1, timestamp: timestamp, adminID: SEED_ACTOR, note: "seed from defaults", changeSetID: null, snapshot: defaultValue };
71
- return Promise.all( [
72
- this.#writeJSON( KEY_CURRENT + configKey, envelope ),
73
- this.#writeJSON( KEY_HISTORY + configKey + ":1", historyEntry )
74
- ] ).then( () => envelope );
75
- } );
76
- }
77
-
78
- /**
79
- * Commits an edit spanning one or more documents as a single change-set. All optimistic-lock checks run before
80
- * any write. Each edit: `{ configKey, value, expectedVersion }`.
81
- *
82
- * @method
83
- * @param {Array<{configKey: string, value: Object, expectedVersion: number}>} edits
84
- * @param {Object} meta
85
- * @param {string} meta.adminID
86
- * @param {string} [meta.note]
87
- * @returns {Promise<{changeSetID: string, versions: Object<string, number>}>}
88
- * @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input or a version conflict (see `details`).
89
- * @public
90
- */
91
- saveChangeSet( edits, meta ) {
92
- if ( !Array.isArray( edits ) || edits.length === 0 || !meta || !meta.adminID ) {
93
- return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-changeset-input" } ) );
94
- }
95
- const keys = edits.map( ( e ) => e.configKey );
96
- if ( new Set( keys ).size !== keys.length ) {
97
- return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "duplicate-configKey-in-changeset" } ) );
98
- }
99
-
100
- return Promise.all( keys.map( ( key ) => this.getCurrent( key ) ) ).then( ( currents ) => {
101
- // Lock check across the whole set first — no writes until every document is confirmed unchanged.
102
- const conflicts = [];
103
- edits.forEach( ( edit, i ) => {
104
- const actual = currents[ i ] ? currents[ i ].version : 0;
105
- if ( edit.expectedVersion !== actual ) conflicts.push( { configKey: edit.configKey, expectedVersion: edit.expectedVersion, actualVersion: actual } );
106
- } );
107
- if ( conflicts.length ) {
108
- throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "version-conflict", conflicts: conflicts } );
109
- }
110
-
111
- const changeSetID = tools.getUUID();
112
- const timestamp = new Date().toISOString();
113
- const note = meta.note || "";
114
- const versions = {};
115
- const writes = [];
116
- edits.forEach( ( edit, i ) => {
117
- const newVersion = ( currents[ i ] ? currents[ i ].version : 0 ) + 1;
118
- versions[ edit.configKey ] = newVersion;
119
- const envelope = { value: edit.value, version: newVersion, updatedAt: timestamp, updatedBy: meta.adminID, changeSetID: changeSetID };
120
- const historyEntry = { version: newVersion, timestamp: timestamp, adminID: meta.adminID, note: note, changeSetID: changeSetID, snapshot: edit.value };
121
- writes.push( this.#writeJSON( KEY_CURRENT + edit.configKey, envelope ) );
122
- writes.push( this.#writeJSON( KEY_HISTORY + edit.configKey + ":" + newVersion, historyEntry ) );
123
- } );
124
- const changeSetRecord = {
125
- changeSetID: changeSetID,
126
- timestamp: timestamp,
127
- adminID: meta.adminID,
128
- note: note,
129
- documents: edits.map( ( edit ) => ( { configKey: edit.configKey, version: versions[ edit.configKey ] } ) )
130
- };
131
- writes.push( this.#writeJSON( KEY_CHANGESET + changeSetID, changeSetRecord ) );
132
-
133
- return Promise.all( writes ).then( () => ( { changeSetID: changeSetID, versions: versions } ) );
134
- } );
135
- }
136
-
137
- /**
138
- * Returns all history entries for a document, ascending by version.
139
- *
140
- * @method
141
- * @param {string} configKey
142
- * @returns {Promise<Array<Object>>}
143
- * @public
144
- */
145
- listHistory( configKey ) {
146
- return cache.instance.matchKeys( KEY_HISTORY + configKey + ":*" ).then( ( keys ) => {
147
- return Promise.all( ( keys || [] ).map( ( k ) => this.#readJSON( k ) ) );
148
- } ).then( ( entries ) => entries.filter( Boolean ).sort( ( a, b ) => a.version - b.version ) );
149
- }
150
-
151
- /**
152
- * Returns a single history snapshot entry for a document version, or `null`.
153
- *
154
- * @method
155
- * @param {string} configKey
156
- * @param {number} version
157
- * @returns {Promise<Object|null>}
158
- * @public
159
- */
160
- getVersion( configKey, version ) {
161
- return this.#readJSON( KEY_HISTORY + configKey + ":" + version );
162
- }
163
-
164
- /**
165
- * Returns a change-set record by id, or `null`.
166
- *
167
- * @method
168
- * @param {string} changeSetID
169
- * @returns {Promise<Object|null>}
170
- * @public
171
- */
172
- getChangeSet( changeSetID ) {
173
- return this.#readJSON( KEY_CHANGESET + changeSetID );
174
- }
175
-
176
- /**
177
- * Returns every change-set record, most-recent first (the cross-document audit feed).
178
- *
179
- * @method
180
- * @returns {Promise<Array<Object>>}
181
- * @public
182
- */
183
- listChangeSets() {
184
- return cache.instance.matchKeys( KEY_CHANGESET + "*" ).then( ( keys ) => {
185
- return Promise.all( ( keys || [] ).map( ( key ) => this.#readJSON( key ) ) );
186
- } ).then( ( records ) => records.filter( Boolean ).sort( ( a, b ) => ( a.timestamp < b.timestamp ? 1 : ( a.timestamp > b.timestamp ? -1 : 0 ) ) ) );
187
- }
188
-
189
- /**
190
- * Restores every document in a prior change-set to that change-set's snapshot, committing it as a *new*
191
- * change-set (restore is never destructive — it moves forward to a past state).
192
- *
193
- * @method
194
- * @param {string} changeSetID
195
- * @param {Object} meta
196
- * @param {string} meta.adminID
197
- * @param {string} [meta.note]
198
- * @returns {Promise<{changeSetID: string, versions: Object<string, number>}>}
199
- * @public
200
- */
201
- restoreChangeSet( changeSetID, meta ) {
202
- if ( !meta || !meta.adminID ) {
203
- return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-restore-input" } ) );
204
- }
205
- return this.getChangeSet( changeSetID ).then( ( record ) => {
206
- if ( !record ) throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-changeset", changeSetID: changeSetID } );
207
- return Promise.all( record.documents.map( ( doc ) => {
208
- return Promise.all( [ this.getVersion( doc.configKey, doc.version ), this.getCurrent( doc.configKey ) ] ).then( ( [ historic, current ] ) => {
209
- return { configKey: doc.configKey, value: historic ? historic.snapshot : null, expectedVersion: current ? current.version : 0 };
210
- } );
211
- } ) ).then( ( edits ) => {
212
- return this.saveChangeSet( edits, { adminID: meta.adminID, note: meta.note || ( "restored from change-set " + changeSetID ) } );
213
- } );
214
- } );
215
- }
216
-
217
- /* Private interface */
218
-
219
- /**
220
- * Reads a whole JSON document at the `$` root and unwraps RedisJSON's array result.
221
- *
222
- * @method
223
- * @param {string} key
224
- * @returns {Promise<Object|null>}
225
- * @private
226
- */
227
- #readJSON( key ) {
228
- return cache.instance.getJSON( key ).then( ( result ) => ( Array.isArray( result ) ? ( result[ 0 ] ?? null ) : ( result ?? null ) ) );
229
- }
230
-
231
- /**
232
- * @method
233
- * @param {string} key
234
- * @param {Object} value
235
- * @returns {Promise}
236
- * @private
237
- */
238
- #writeJSON( key, value ) {
239
- return cache.instance.setJSON( key, value );
240
- }
241
-
242
- }
243
-
244
- const instance = new ConfigStore();
245
- module.exports = ConfigStore;
246
- module.exports.instance = Object.freeze( 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 cache = require( "@ti-engine/core/cache" );
10
+ const tools = require( "@ti-engine/core/tools" );
11
+ const exceptions = require( "@ti-engine/core/exceptions" );
12
+
13
+ const KEY_CURRENT = "ti:config:cur:"; // + configKey → current document envelope
14
+ const KEY_HISTORY = "ti:config:hist:"; // + configKey + ":" + version → history snapshot entry
15
+ const KEY_CHANGESET = "ti:config:cs:"; // + changeSetID → change-set record
16
+ const SEED_ACTOR = "system:seed";
17
+
18
+ /**
19
+ * A versioned, change-set-aware configuration store backed by the common memory cache (RedisJSON).
20
+ * <br/>
21
+ * Each editable configuration is a *document* identified by a `configKey`. Every committed edit:
22
+ * - bumps the document's monotonic `version`,
23
+ * - writes a full **snapshot** to history (enabling restore),
24
+ * - and is correlated with the other documents written in the same logical edit via a shared **change-set** id,
25
+ * so a multi-document edit (and its restore) is treated as one unit even though storage is per-document.
26
+ * <br/>
27
+ * Optimistic locking: callers pass the `expectedVersion` they edited from; the save is rejected if any document
28
+ * moved on in the meantime. This component is storage-only — schema/semantic validation is a separate pipeline
29
+ * that must run *before* {@link ConfigStore#saveChangeSet}.
30
+ * <br/>
31
+ * NOTE: true cross-document atomicity is not provided (the cache exposes per-key commands only). All locks are
32
+ * checked *before* any write, so the common conflict case is safe; a mid-write process failure can leave a
33
+ * partially-applied change-set, detectable via the change-set record. Hardening (a Lua/MULTI write) is deferred.
34
+ *
35
+ * @class ConfigStore
36
+ * @public
37
+ */
38
+ class ConfigStore {
39
+
40
+ /* Public interface */
41
+
42
+ /**
43
+ * Returns the current envelope `{ value, version, updatedAt, updatedBy, changeSetID }` for a configuration
44
+ * document, or `null` if it has never been written.
45
+ *
46
+ * @method
47
+ * @param {string} configKey
48
+ * @returns {Promise<Object|null>}
49
+ * @public
50
+ */
51
+ getCurrent( configKey ) {
52
+ return this.#readJSON( KEY_CURRENT + configKey );
53
+ }
54
+
55
+ /**
56
+ * Writes the default value as version 1 only if the document does not yet exist (idempotent bootstrap).
57
+ * Resolves with the current envelope either way.
58
+ *
59
+ * @method
60
+ * @param {string} configKey
61
+ * @param {Object} defaultValue
62
+ * @returns {Promise<Object>}
63
+ * @public
64
+ */
65
+ seedIfEmpty( configKey, defaultValue ) {
66
+ return this.getCurrent( configKey ).then( ( current ) => {
67
+ if ( current ) return current;
68
+ const timestamp = new Date().toISOString();
69
+ const envelope = { value: defaultValue, version: 1, updatedAt: timestamp, updatedBy: SEED_ACTOR, changeSetID: null };
70
+ const historyEntry = { version: 1, timestamp: timestamp, adminID: SEED_ACTOR, note: "seed from defaults", changeSetID: null, snapshot: defaultValue };
71
+ return Promise.all( [
72
+ this.#writeJSON( KEY_CURRENT + configKey, envelope ),
73
+ this.#writeJSON( KEY_HISTORY + configKey + ":1", historyEntry )
74
+ ] ).then( () => envelope );
75
+ } );
76
+ }
77
+
78
+ /**
79
+ * Commits an edit spanning one or more documents as a single change-set. All optimistic-lock checks run before
80
+ * any write. Each edit: `{ configKey, value, expectedVersion }`.
81
+ *
82
+ * @method
83
+ * @param {Array<{configKey: string, value: Object, expectedVersion: number}>} edits
84
+ * @param {Object} meta
85
+ * @param {string} meta.adminID
86
+ * @param {string} [meta.note]
87
+ * @returns {Promise<{changeSetID: string, versions: Object<string, number>}>}
88
+ * @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input or a version conflict (see `details`).
89
+ * @public
90
+ */
91
+ saveChangeSet( edits, meta ) {
92
+ if ( !Array.isArray( edits ) || edits.length === 0 || !meta || !meta.adminID ) {
93
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-changeset-input" } ) );
94
+ }
95
+ const keys = edits.map( ( e ) => e.configKey );
96
+ if ( new Set( keys ).size !== keys.length ) {
97
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "duplicate-configKey-in-changeset" } ) );
98
+ }
99
+
100
+ return Promise.all( keys.map( ( key ) => this.getCurrent( key ) ) ).then( ( currents ) => {
101
+ // Lock check across the whole set first — no writes until every document is confirmed unchanged.
102
+ const conflicts = [];
103
+ edits.forEach( ( edit, i ) => {
104
+ const actual = currents[ i ] ? currents[ i ].version : 0;
105
+ if ( edit.expectedVersion !== actual ) conflicts.push( { configKey: edit.configKey, expectedVersion: edit.expectedVersion, actualVersion: actual } );
106
+ } );
107
+ if ( conflicts.length ) {
108
+ throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "version-conflict", conflicts: conflicts } );
109
+ }
110
+
111
+ const changeSetID = tools.getUUID();
112
+ const timestamp = new Date().toISOString();
113
+ const note = meta.note || "";
114
+ const versions = {};
115
+ const writes = [];
116
+ edits.forEach( ( edit, i ) => {
117
+ const newVersion = ( currents[ i ] ? currents[ i ].version : 0 ) + 1;
118
+ versions[ edit.configKey ] = newVersion;
119
+ const envelope = { value: edit.value, version: newVersion, updatedAt: timestamp, updatedBy: meta.adminID, changeSetID: changeSetID };
120
+ const historyEntry = { version: newVersion, timestamp: timestamp, adminID: meta.adminID, note: note, changeSetID: changeSetID, snapshot: edit.value };
121
+ writes.push( this.#writeJSON( KEY_CURRENT + edit.configKey, envelope ) );
122
+ writes.push( this.#writeJSON( KEY_HISTORY + edit.configKey + ":" + newVersion, historyEntry ) );
123
+ } );
124
+ const changeSetRecord = {
125
+ changeSetID: changeSetID,
126
+ timestamp: timestamp,
127
+ adminID: meta.adminID,
128
+ note: note,
129
+ documents: edits.map( ( edit ) => ( { configKey: edit.configKey, version: versions[ edit.configKey ] } ) )
130
+ };
131
+ writes.push( this.#writeJSON( KEY_CHANGESET + changeSetID, changeSetRecord ) );
132
+
133
+ return Promise.all( writes ).then( () => ( { changeSetID: changeSetID, versions: versions } ) );
134
+ } );
135
+ }
136
+
137
+ /**
138
+ * Returns all history entries for a document, ascending by version.
139
+ *
140
+ * @method
141
+ * @param {string} configKey
142
+ * @returns {Promise<Array<Object>>}
143
+ * @public
144
+ */
145
+ listHistory( configKey ) {
146
+ return cache.instance.matchKeys( KEY_HISTORY + configKey + ":*" ).then( ( keys ) => {
147
+ return Promise.all( ( keys || [] ).map( ( k ) => this.#readJSON( k ) ) );
148
+ } ).then( ( entries ) => entries.filter( Boolean ).sort( ( a, b ) => a.version - b.version ) );
149
+ }
150
+
151
+ /**
152
+ * Returns a single history snapshot entry for a document version, or `null`.
153
+ *
154
+ * @method
155
+ * @param {string} configKey
156
+ * @param {number} version
157
+ * @returns {Promise<Object|null>}
158
+ * @public
159
+ */
160
+ getVersion( configKey, version ) {
161
+ return this.#readJSON( KEY_HISTORY + configKey + ":" + version );
162
+ }
163
+
164
+ /**
165
+ * Returns a change-set record by id, or `null`.
166
+ *
167
+ * @method
168
+ * @param {string} changeSetID
169
+ * @returns {Promise<Object|null>}
170
+ * @public
171
+ */
172
+ getChangeSet( changeSetID ) {
173
+ return this.#readJSON( KEY_CHANGESET + changeSetID );
174
+ }
175
+
176
+ /**
177
+ * Returns every change-set record, most-recent first (the cross-document audit feed).
178
+ *
179
+ * @method
180
+ * @returns {Promise<Array<Object>>}
181
+ * @public
182
+ */
183
+ listChangeSets() {
184
+ return cache.instance.matchKeys( KEY_CHANGESET + "*" ).then( ( keys ) => {
185
+ return Promise.all( ( keys || [] ).map( ( key ) => this.#readJSON( key ) ) );
186
+ } ).then( ( records ) => records.filter( Boolean ).sort( ( a, b ) => ( a.timestamp < b.timestamp ? 1 : ( a.timestamp > b.timestamp ? -1 : 0 ) ) ) );
187
+ }
188
+
189
+ /**
190
+ * Restores every document in a prior change-set to that change-set's snapshot, committing it as a *new*
191
+ * change-set (restore is never destructive — it moves forward to a past state).
192
+ *
193
+ * @method
194
+ * @param {string} changeSetID
195
+ * @param {Object} meta
196
+ * @param {string} meta.adminID
197
+ * @param {string} [meta.note]
198
+ * @returns {Promise<{changeSetID: string, versions: Object<string, number>}>}
199
+ * @public
200
+ */
201
+ restoreChangeSet( changeSetID, meta ) {
202
+ if ( !meta || !meta.adminID ) {
203
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-restore-input" } ) );
204
+ }
205
+ return this.getChangeSet( changeSetID ).then( ( record ) => {
206
+ if ( !record ) throw exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-changeset", changeSetID: changeSetID } );
207
+ return Promise.all( record.documents.map( ( doc ) => {
208
+ return Promise.all( [ this.getVersion( doc.configKey, doc.version ), this.getCurrent( doc.configKey ) ] ).then( ( [ historic, current ] ) => {
209
+ return { configKey: doc.configKey, value: historic ? historic.snapshot : null, expectedVersion: current ? current.version : 0 };
210
+ } );
211
+ } ) ).then( ( edits ) => {
212
+ return this.saveChangeSet( edits, { adminID: meta.adminID, note: meta.note || ( "restored from change-set " + changeSetID ) } );
213
+ } );
214
+ } );
215
+ }
216
+
217
+ /* Private interface */
218
+
219
+ /**
220
+ * Reads a whole JSON document at the `$` root and unwraps RedisJSON's array result.
221
+ *
222
+ * @method
223
+ * @param {string} key
224
+ * @returns {Promise<Object|null>}
225
+ */
226
+ #readJSON( key ) {
227
+ return cache.instance.getJSON( key ).then( ( result ) => ( Array.isArray( result ) ? ( result[ 0 ] ?? null ) : ( result ?? null ) ) );
228
+ }
229
+
230
+ /**
231
+ * @method
232
+ * @param {string} key
233
+ * @param {Object} value
234
+ * @returns {Promise}
235
+ */
236
+ #writeJSON( key, value ) {
237
+ return cache.instance.setJSON( key, value );
238
+ }
239
+
240
+ }
241
+
242
+ const instance = new ConfigStore();
243
+ module.exports = ConfigStore;
244
+ ConfigStore.instance = Object.freeze( instance );
@@ -1,26 +1,28 @@
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
- /**
10
- * @callback TiSessionCallback
11
- * @param {Error|null} [error]
12
- * @returns {void}
13
- */
14
-
15
- /**
16
- * @typedef {Object} TiSession
17
- * @property {string} id
18
- * @property {Object} [user]
19
- * @property {TiLocalizationLanguage} [language]
20
- * @property {Object} [cookie]
21
- * @property {Object} [oidc]
22
- * @property {string} [csrfToken]
23
- * @property {function(TiSessionCallback): TiSession} regenerate
24
- * @property {function(TiSessionCallback): TiSession} destroy
25
- * @property {function(TiSessionCallback=): TiSession} save
26
- */
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
+ /** @import { TiLocalizationLanguage } from "@ti-engine/core/localization" */
10
+
11
+ /**
12
+ * @callback TiSessionCallback
13
+ * @param {Error|null} [error]
14
+ * @returns {void}
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} TiSession
19
+ * @property {string} id
20
+ * @property {Object} [user]
21
+ * @property {TiLocalizationLanguage} [language]
22
+ * @property {Object} [cookie]
23
+ * @property {Object} [oidc]
24
+ * @property {string} [csrfToken]
25
+ * @property {(callback: TiSessionCallback) => TiSession} regenerate
26
+ * @property {(callback: TiSessionCallback) => TiSession} destroy
27
+ * @property {(callback?: TiSessionCallback) => TiSession} save
28
+ */