@ti-engine/web-framework 1.23.0 → 1.24.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/CHANGELOG.md +26 -0
- package/bin/web-server.js +3 -0
- package/components/admin-config-handlers.js +31 -0
- package/components/config-drift.js +141 -0
- package/components/config-service.js +97 -0
- package/package.json +9 -1
- package/types/components/admin-config-handlers.d.ts +3 -0
- package/types/components/config-drift.d.ts +27 -0
- package/types/components/config-service.d.ts +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
|
4
4
|
|
|
5
|
+
## Version 1.24.0
|
|
6
|
+
|
|
7
|
+
A configuration file change shipped in a release could never reach a deployment that had already been seeded. The
|
|
8
|
+
store writes a file default only when the document has never been written, and the consuming application then lets
|
|
9
|
+
the stored value overwrite the file value on every boot — so the file default is consulted exactly once in a
|
|
10
|
+
deployment's lifetime. Restore could not help either, since it replays a previous version and the oldest version
|
|
11
|
+
*is* the stale one. The framework now detects that difference and lets an admin apply it deliberately.
|
|
12
|
+
|
|
13
|
+
* feat(config-drift): new `#config-drift` module — a pure structural diff between a document's registered file
|
|
14
|
+
default and its stored value. Recurses into objects to report leaf paths, **set-diffs arrays of primitives** so a
|
|
15
|
+
code-list change reads as `+27 codes` rather than an opaque "changed", and compares arrays of objects atomically.
|
|
16
|
+
Paths use the same dot/bracket dialect as schema validation issues
|
|
17
|
+
* feat(config-service): `getDrift`, `listDrift` and `applyDefaults`. Applying routes through `applyEdits`, so a
|
|
18
|
+
file default lands validated, versioned, correlated into one change-set, in the audit feed, and restorable —
|
|
19
|
+
never as a side-channel write
|
|
20
|
+
* feat(config-service): interdependent documents apply as a **single** change-set, which is required rather than
|
|
21
|
+
merely convenient: a semantic validator resolves its siblings at their *pending* value, so a document whose
|
|
22
|
+
constraint spans another can only pass when both are applied together
|
|
23
|
+
* feat(admin-config-handlers): `GET /admin/config/drift`, `GET /admin/config/drift/:configKey` and
|
|
24
|
+
`POST /admin/config/drift/apply`, all admin-gated
|
|
25
|
+
* build(release): bump package version from `1.23.0` to `1.24.0`
|
|
26
|
+
|
|
27
|
+
**Note on statuses:** `absent` (never seeded) is deliberately distinct from `drifted`. A document that is registered
|
|
28
|
+
but never seeded is not a problem to act on, and folding the two together would flag it on every boot of a clean
|
|
29
|
+
install — training operators to ignore exactly the signal this feature exists to raise.
|
|
30
|
+
|
|
5
31
|
## Version 1.23.0
|
|
6
32
|
|
|
7
33
|
Local (username/password) authentication is real. It had never been implemented: the constructor overwrote whatever
|
package/bin/web-server.js
CHANGED
|
@@ -585,6 +585,9 @@ class TiWebServer extends ServiceConsumer {
|
|
|
585
585
|
this.#webServer.get( "/admin/config/changes/:changeSetID", requireAdmin, adminConfigHandlers.getChange( service ) );
|
|
586
586
|
this.#webServer.post( "/admin/config/changes/:changeSetID/restore", requireAdmin, adminConfigHandlers.restoreChangeSet( service ) );
|
|
587
587
|
this.#webServer.get( "/admin/config/export", requireAdmin, adminConfigHandlers.exportBundle( service ) );
|
|
588
|
+
this.#webServer.get( "/admin/config/drift", requireAdmin, adminConfigHandlers.listDrift( service ) );
|
|
589
|
+
this.#webServer.get( "/admin/config/drift/:configKey", requireAdmin, adminConfigHandlers.getDrift( service ) );
|
|
590
|
+
this.#webServer.post( "/admin/config/drift/apply", requireAdmin, adminConfigHandlers.applyDefaults( service ) );
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
/**
|
|
@@ -93,3 +93,34 @@ module.exports.exportBundle = ( service ) => ( request, response, next ) => {
|
|
|
93
93
|
response.status( exceptions.httpCode.C_200 ).send( JSON.stringify( bundle, null, 2 ) );
|
|
94
94
|
} ).catch( ( error ) => forward( next, error ) );
|
|
95
95
|
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `GET /admin/config/drift` — drift summaries for every registered document.
|
|
99
|
+
*
|
|
100
|
+
* @param {ConfigService} service
|
|
101
|
+
* @returns {ExpressHandler} The Express handler.
|
|
102
|
+
*/
|
|
103
|
+
module.exports.listDrift = ( service ) => ( request, response, next ) => {
|
|
104
|
+
service.listDrift().then( ( drift ) => sendData( response, drift ) ).catch( ( error ) => forward( next, error ) );
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `GET /admin/config/drift/:configKey` — one document's drift, including the full entry list.
|
|
109
|
+
*
|
|
110
|
+
* @param {ConfigService} service
|
|
111
|
+
* @returns {ExpressHandler} The Express handler.
|
|
112
|
+
*/
|
|
113
|
+
module.exports.getDrift = ( service ) => ( request, response, next ) => {
|
|
114
|
+
service.getDrift( request.params.configKey ).then( ( drift ) => sendData( response, drift ) ).catch( ( error ) => forward( next, error ) );
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* `POST /admin/config/drift/apply` — applies the file defaults for the named documents as one change-set.
|
|
119
|
+
*
|
|
120
|
+
* @param {ConfigService} service
|
|
121
|
+
* @returns {ExpressHandler} The Express handler.
|
|
122
|
+
*/
|
|
123
|
+
module.exports.applyDefaults = ( service ) => ( request, response, next ) => {
|
|
124
|
+
const body = request.body || {};
|
|
125
|
+
service.applyDefaults( body.configKeys, { adminID: adminID( request ), note: body.note } ).then( ( result ) => sendData( response, result ) ).catch( ( error ) => forward( next, error ) );
|
|
126
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
* Pure structural diff between a configuration document's file default and the value currently held in the store.
|
|
11
|
+
* <br/>
|
|
12
|
+
* The store seeds from a file default exactly once ({@link ConfigStore#seedIfEmpty}), so a release that changes a
|
|
13
|
+
* config file changes nothing an already-seeded deployment serves. This module is the detection half of the remedy:
|
|
14
|
+
* it answers "how does the shipped default differ from what this deployment is running", in terms legible enough for
|
|
15
|
+
* an admin to judge whether applying it is safe.
|
|
16
|
+
* <br/>
|
|
17
|
+
* No I/O — the caller supplies both values.
|
|
18
|
+
*
|
|
19
|
+
* @module config-drift
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { isDeepStrictEqual } = require( "node:util" );
|
|
23
|
+
|
|
24
|
+
const STATUS_IN_SYNC = "in-sync";
|
|
25
|
+
const STATUS_DRIFTED = "drifted";
|
|
26
|
+
const STATUS_ABSENT = "absent";
|
|
27
|
+
const STATUS_NO_DEFAULT = "no-default";
|
|
28
|
+
|
|
29
|
+
const KIND_ADDED = "added";
|
|
30
|
+
const KIND_REMOVED = "removed";
|
|
31
|
+
const KIND_CHANGED = "changed";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} ConfigDriftEntry
|
|
35
|
+
* @property {string} path Dot/bracket data path, matching the dialect used for schema validation issues.
|
|
36
|
+
* @property {string} kind One of "added", "removed", "changed".
|
|
37
|
+
* @property {number} [addedMembers] For a primitive array: how many members the file default adds.
|
|
38
|
+
* @property {number} [removedMembers] For a primitive array: how many members the file default drops.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {*} value
|
|
43
|
+
* @returns {boolean} True for a non-null, non-array object.
|
|
44
|
+
*/
|
|
45
|
+
function isPlainObject( value ) {
|
|
46
|
+
return value !== null && typeof value === "object" && !Array.isArray( value );
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {*} value
|
|
51
|
+
* @returns {boolean} True for an array holding no objects (a code list, a set of flags, …).
|
|
52
|
+
*/
|
|
53
|
+
function isPrimitiveArray( value ) {
|
|
54
|
+
return Array.isArray( value ) && value.every( ( item ) => item === null || typeof item !== "object" );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} base
|
|
59
|
+
* @param {string} key
|
|
60
|
+
* @returns {string} The child path, numeric keys in bracket notation.
|
|
61
|
+
*/
|
|
62
|
+
function joinPath( base, key ) {
|
|
63
|
+
return ( /^\d+$/.test( key ) ) ? `${ base }[${ key }]` : `${ base }.${ key }`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursive worker. Appends to `entries` in place.
|
|
68
|
+
*
|
|
69
|
+
* @param {*} fileValue
|
|
70
|
+
* @param {*} storedValue
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @param {ConfigDriftEntry[]} entries
|
|
73
|
+
*/
|
|
74
|
+
function diffValue( fileValue, storedValue, path, entries ) {
|
|
75
|
+
if ( isPlainObject( fileValue ) && isPlainObject( storedValue ) ) {
|
|
76
|
+
const keys = new Set( [ ...Object.keys( fileValue ), ...Object.keys( storedValue ) ] );
|
|
77
|
+
for ( const key of keys ) {
|
|
78
|
+
const childPath = joinPath( path, key );
|
|
79
|
+
const inFile = Object.prototype.hasOwnProperty.call( fileValue, key );
|
|
80
|
+
const inStored = Object.prototype.hasOwnProperty.call( storedValue, key );
|
|
81
|
+
if ( inFile && !inStored ) {
|
|
82
|
+
entries.push( { path: childPath, kind: KIND_ADDED } );
|
|
83
|
+
} else if ( !inFile && inStored ) {
|
|
84
|
+
entries.push( { path: childPath, kind: KIND_REMOVED } );
|
|
85
|
+
} else {
|
|
86
|
+
diffValue( fileValue[ key ], storedValue[ key ], childPath, entries );
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A list of codes is a set, not a sequence: report which members moved, and treat a pure reorder as no change.
|
|
93
|
+
// This is what turns "role-family-competencies changed" into the far more useful "QE +27 codes".
|
|
94
|
+
// <br/>
|
|
95
|
+
// Set semantics also mean MULTIPLICITY is ignored — `[ "A", "A" ]` and `[ "A" ]` compare as in-sync even though
|
|
96
|
+
// applying would replace one with the other. That is deliberate for a list of codes, where a repeated member is
|
|
97
|
+
// a data error rather than a meaningful difference, and it is unreachable for the documents shipped here: the
|
|
98
|
+
// array-valued competence schemas all declare `uniqueItems`, so a duplicate cannot pass validation on save. A
|
|
99
|
+
// consumer whose arrays are genuinely multisets should not model them as primitive arrays for this diff.
|
|
100
|
+
if ( isPrimitiveArray( fileValue ) && isPrimitiveArray( storedValue ) ) {
|
|
101
|
+
const storedMembers = new Set( storedValue );
|
|
102
|
+
const fileMembers = new Set( fileValue );
|
|
103
|
+
const addedMembers = fileValue.filter( ( item ) => !storedMembers.has( item ) ).length;
|
|
104
|
+
const removedMembers = storedValue.filter( ( item ) => !fileMembers.has( item ) ).length;
|
|
105
|
+
if ( addedMembers > 0 || removedMembers > 0 ) {
|
|
106
|
+
entries.push( { path: path, kind: KIND_CHANGED, addedMembers: addedMembers, removedMembers: removedMembers } );
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ( !isDeepStrictEqual( fileValue, storedValue ) ) {
|
|
112
|
+
entries.push( { path: path, kind: KIND_CHANGED } );
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Diffs a document's registered file default against its stored value.
|
|
118
|
+
*
|
|
119
|
+
* @method
|
|
120
|
+
* @param {*} fileDefault The value registered with {@link ConfigRegistry#register}; `undefined` when none was.
|
|
121
|
+
* @param {*} storedValue The value currently in the store; `null`/`undefined` when never written.
|
|
122
|
+
* @returns {{status: string, entries: ConfigDriftEntry[], counts: {added: number, removed: number, changed: number}}}
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
module.exports.diffDocument = ( fileDefault, storedValue ) => {
|
|
126
|
+
if ( fileDefault === undefined ) {
|
|
127
|
+
return { status: STATUS_NO_DEFAULT, entries: [], counts: { added: 0, removed: 0, changed: 0 } };
|
|
128
|
+
}
|
|
129
|
+
if ( storedValue === undefined || storedValue === null ) {
|
|
130
|
+
return { status: STATUS_ABSENT, entries: [], counts: { added: 0, removed: 0, changed: 0 } };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entries = [];
|
|
134
|
+
diffValue( fileDefault, storedValue, "", entries );
|
|
135
|
+
|
|
136
|
+
const counts = { added: 0, removed: 0, changed: 0 };
|
|
137
|
+
for ( const entry of entries ) {
|
|
138
|
+
counts[ entry.kind ] += 1;
|
|
139
|
+
}
|
|
140
|
+
return { status: ( entries.length > 0 ) ? STATUS_DRIFTED : STATUS_IN_SYNC, entries: entries, counts: counts };
|
|
141
|
+
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const exceptions = require( "@ti-engine/core/exceptions" );
|
|
10
|
+
const configDrift = require( "#config-drift" );
|
|
10
11
|
|
|
11
12
|
/** @import ConfigChangeNotifier from "#config-change-notifier" */
|
|
12
13
|
/** @import ConfigRegistry from "#config-registry" */
|
|
@@ -307,6 +308,102 @@ class ConfigService {
|
|
|
307
308
|
} ) );
|
|
308
309
|
}
|
|
309
310
|
|
|
311
|
+
/* Public interface — drift against file defaults */
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Compares a document's registered file default against the value currently in the store. This is how a
|
|
315
|
+
* configuration change shipped in a release becomes visible on a deployment that was seeded before it — the
|
|
316
|
+
* store seeds only once, so a later file change is otherwise invisible.
|
|
317
|
+
*
|
|
318
|
+
* @method
|
|
319
|
+
* @param {string} configKey
|
|
320
|
+
* @returns {Promise<{configKey: string, status: string, counts: Object, entries: Array, storedVersion: number, editable: boolean, label: string}>}
|
|
321
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
322
|
+
* @public
|
|
323
|
+
*/
|
|
324
|
+
getDrift( configKey ) {
|
|
325
|
+
if ( !this.#registry.has( configKey ) ) {
|
|
326
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKey: configKey } ) );
|
|
327
|
+
}
|
|
328
|
+
const metadata = this.#registry.metadataFor( configKey ) || {};
|
|
329
|
+
return this.#store.getCurrent( configKey ).then( ( current ) => {
|
|
330
|
+
const diff = configDrift.diffDocument( this.#registry.getDefault( configKey ), current ? current.value : null );
|
|
331
|
+
return {
|
|
332
|
+
configKey: configKey,
|
|
333
|
+
status: diff.status,
|
|
334
|
+
counts: diff.counts,
|
|
335
|
+
entries: diff.entries,
|
|
336
|
+
storedVersion: current ? current.version : 0,
|
|
337
|
+
editable: metadata.editable !== false,
|
|
338
|
+
label: metadata.label || configKey
|
|
339
|
+
};
|
|
340
|
+
} );
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Drift summaries for every registered document. This still computes each document's full entry list internally
|
|
345
|
+
* (it delegates to {@link ConfigService#getDrift} per document) — the saving is in the response shape, not the
|
|
346
|
+
* computation: `entries` is omitted here to keep the payload small enough for a landing screen and a startup log,
|
|
347
|
+
* where only the counts are shown.
|
|
348
|
+
*
|
|
349
|
+
* @method
|
|
350
|
+
* @returns {Promise<Array<Object>>}
|
|
351
|
+
* @public
|
|
352
|
+
*/
|
|
353
|
+
listDrift() {
|
|
354
|
+
return Promise.all( this.#registry.list().map( ( configKey ) => {
|
|
355
|
+
return this.getDrift( configKey ).then( ( drift ) => ( {
|
|
356
|
+
configKey: drift.configKey,
|
|
357
|
+
status: drift.status,
|
|
358
|
+
counts: drift.counts,
|
|
359
|
+
storedVersion: drift.storedVersion,
|
|
360
|
+
editable: drift.editable,
|
|
361
|
+
label: drift.label
|
|
362
|
+
} ) );
|
|
363
|
+
} ) );
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Applies the registered file defaults for the given documents, as a single validated change-set.
|
|
368
|
+
* <br/>
|
|
369
|
+
* Routing through {@link ConfigService#applyEdits} is deliberate: the application is schema- and
|
|
370
|
+
* semantically validated, versioned, correlated into one change-set, added to the audit feed, and restorable —
|
|
371
|
+
* and, because a validator sees its siblings at their *pending* value, interdependent documents applied
|
|
372
|
+
* together validate against each other rather than against the stale stored state.
|
|
373
|
+
*
|
|
374
|
+
* @method
|
|
375
|
+
* @param {string[]} configKeys
|
|
376
|
+
* @param {Object} meta
|
|
377
|
+
* @param {string} meta.adminID
|
|
378
|
+
* @param {string} [meta.note]
|
|
379
|
+
* @returns {Promise<{ok: true, changeSetID: string, versions: Object}|{ok: false, errors: Object}>}
|
|
380
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input, an unknown key, or a key with no default.
|
|
381
|
+
* @public
|
|
382
|
+
*/
|
|
383
|
+
applyDefaults( configKeys, meta ) {
|
|
384
|
+
if ( !Array.isArray( configKeys ) || configKeys.length === 0 || !meta || !meta.adminID ) {
|
|
385
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-apply-defaults-input" } ) );
|
|
386
|
+
}
|
|
387
|
+
const keys = Array.from( new Set( configKeys ) );
|
|
388
|
+
const unknown = keys.filter( ( key ) => !this.#registry.has( key ) );
|
|
389
|
+
if ( unknown.length > 0 ) {
|
|
390
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKeys: unknown } ) );
|
|
391
|
+
}
|
|
392
|
+
const withoutDefault = keys.filter( ( key ) => this.#registry.getDefault( key ) === undefined );
|
|
393
|
+
if ( withoutDefault.length > 0 ) {
|
|
394
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "no-default", configKeys: withoutDefault } ) );
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return Promise.all( keys.map( ( key ) => this.#store.getCurrent( key ) ) ).then( ( currents ) => {
|
|
398
|
+
const edits = keys.map( ( key, index ) => ( {
|
|
399
|
+
configKey: key,
|
|
400
|
+
value: this.#registry.getDefault( key ),
|
|
401
|
+
expectedVersion: currents[ index ] ? currents[ index ].version : 0
|
|
402
|
+
} ) );
|
|
403
|
+
return this.applyEdits( edits, { adminID: meta.adminID, note: meta.note || "applied file defaults" } );
|
|
404
|
+
} );
|
|
405
|
+
}
|
|
406
|
+
|
|
310
407
|
/**
|
|
311
408
|
* Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
|
|
312
409
|
* Used by an application to bring its file defaults into the store at startup before serving live config.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/web-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "A web-framework based on the ti-engine. It provides a customizable ready-to-use web-server microservice and a set of tools for creating web applications. NOTICE: This is still a work in progress and the full architecture, design, and functionality are not available!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ti-engine",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
|
|
16
16
|
"license": "GPL-3.0-or-later",
|
|
17
17
|
"exports": {
|
|
18
|
+
"./config-drift": {
|
|
19
|
+
"types": "./types/components/config-drift.d.ts",
|
|
20
|
+
"default": "./components/config-drift.js"
|
|
21
|
+
},
|
|
18
22
|
"./config-management": {
|
|
19
23
|
"types": "./types/components/config-service.d.ts",
|
|
20
24
|
"default": "./components/config-service.js"
|
|
@@ -57,6 +61,10 @@
|
|
|
57
61
|
"types": "./types/components/config-change-notifier.d.ts",
|
|
58
62
|
"default": "./components/config-change-notifier.js"
|
|
59
63
|
},
|
|
64
|
+
"#config-drift": {
|
|
65
|
+
"types": "./types/components/config-drift.d.ts",
|
|
66
|
+
"default": "./components/config-drift.js"
|
|
67
|
+
},
|
|
60
68
|
"#config-registry": {
|
|
61
69
|
"types": "./types/components/config-registry.d.ts",
|
|
62
70
|
"default": "./components/config-registry.js"
|
|
@@ -7,5 +7,8 @@ export declare var listChanges: (service: any) => (request: any, response: any,
|
|
|
7
7
|
export declare var getChange: (service: any) => (request: any, response: any, next: any) => void;
|
|
8
8
|
export declare var restoreChangeSet: (service: any) => (request: any, response: any, next: any) => void;
|
|
9
9
|
export declare var exportBundle: (service: any) => (request: any, response: any, next: any) => void;
|
|
10
|
+
export declare var listDrift: (service: ConfigService) => ExpressHandler;
|
|
11
|
+
export declare var getDrift: (service: ConfigService) => ExpressHandler;
|
|
12
|
+
export declare var applyDefaults: (service: ConfigService) => ExpressHandler;
|
|
10
13
|
import type ConfigService from "#config-service";
|
|
11
14
|
import type { ExpressHandler } from "#web-handlers";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare var diffDocument: (fileDefault: any, storedValue: any) => {
|
|
2
|
+
status: string;
|
|
3
|
+
entries: ConfigDriftEntry[];
|
|
4
|
+
counts: {
|
|
5
|
+
added: number;
|
|
6
|
+
removed: number;
|
|
7
|
+
changed: number;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type ConfigDriftEntry = {
|
|
11
|
+
/**
|
|
12
|
+
* Dot/bracket data path, matching the dialect used for schema validation issues.
|
|
13
|
+
*/
|
|
14
|
+
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* One of "added", "removed", "changed".
|
|
17
|
+
*/
|
|
18
|
+
kind: string;
|
|
19
|
+
/**
|
|
20
|
+
* For a primitive array: how many members the file default adds.
|
|
21
|
+
*/
|
|
22
|
+
addedMembers?: number;
|
|
23
|
+
/**
|
|
24
|
+
* For a primitive array: how many members the file default drops.
|
|
25
|
+
*/
|
|
26
|
+
removedMembers?: number;
|
|
27
|
+
};
|
|
@@ -191,6 +191,65 @@ declare class ConfigService {
|
|
|
191
191
|
value: Object;
|
|
192
192
|
}>;
|
|
193
193
|
}>;
|
|
194
|
+
/**
|
|
195
|
+
* Compares a document's registered file default against the value currently in the store. This is how a
|
|
196
|
+
* configuration change shipped in a release becomes visible on a deployment that was seeded before it — the
|
|
197
|
+
* store seeds only once, so a later file change is otherwise invisible.
|
|
198
|
+
*
|
|
199
|
+
* @method
|
|
200
|
+
* @param {string} configKey
|
|
201
|
+
* @returns {Promise<{configKey: string, status: string, counts: Object, entries: Array, storedVersion: number, editable: boolean, label: string}>}
|
|
202
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
203
|
+
* @public
|
|
204
|
+
*/
|
|
205
|
+
getDrift(configKey: string): Promise<{
|
|
206
|
+
configKey: string;
|
|
207
|
+
status: string;
|
|
208
|
+
counts: Object;
|
|
209
|
+
entries: any[];
|
|
210
|
+
storedVersion: number;
|
|
211
|
+
editable: boolean;
|
|
212
|
+
label: string;
|
|
213
|
+
}>;
|
|
214
|
+
/**
|
|
215
|
+
* Drift summaries for every registered document. This still computes each document's full entry list internally
|
|
216
|
+
* (it delegates to {@link ConfigService#getDrift} per document) — the saving is in the response shape, not the
|
|
217
|
+
* computation: `entries` is omitted here to keep the payload small enough for a landing screen and a startup log,
|
|
218
|
+
* where only the counts are shown.
|
|
219
|
+
*
|
|
220
|
+
* @method
|
|
221
|
+
* @returns {Promise<Array<Object>>}
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
224
|
+
listDrift(): Promise<Array<Object>>;
|
|
225
|
+
/**
|
|
226
|
+
* Applies the registered file defaults for the given documents, as a single validated change-set.
|
|
227
|
+
* <br/>
|
|
228
|
+
* Routing through {@link ConfigService#applyEdits} is deliberate: the application is schema- and
|
|
229
|
+
* semantically validated, versioned, correlated into one change-set, added to the audit feed, and restorable —
|
|
230
|
+
* and, because a validator sees its siblings at their *pending* value, interdependent documents applied
|
|
231
|
+
* together validate against each other rather than against the stale stored state.
|
|
232
|
+
*
|
|
233
|
+
* @method
|
|
234
|
+
* @param {string[]} configKeys
|
|
235
|
+
* @param {Object} meta
|
|
236
|
+
* @param {string} meta.adminID
|
|
237
|
+
* @param {string} [meta.note]
|
|
238
|
+
* @returns {Promise<{ok: true, changeSetID: string, versions: Object}|{ok: false, errors: Object}>}
|
|
239
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input, an unknown key, or a key with no default.
|
|
240
|
+
* @public
|
|
241
|
+
*/
|
|
242
|
+
applyDefaults(configKeys: string[], meta: {
|
|
243
|
+
adminID: string;
|
|
244
|
+
note?: string;
|
|
245
|
+
}): Promise<{
|
|
246
|
+
ok: true;
|
|
247
|
+
changeSetID: string;
|
|
248
|
+
versions: Object;
|
|
249
|
+
} | {
|
|
250
|
+
ok: false;
|
|
251
|
+
errors: Object;
|
|
252
|
+
}>;
|
|
194
253
|
/**
|
|
195
254
|
* Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
|
|
196
255
|
* Used by an application to bring its file defaults into the store at startup before serving live config.
|