@ti-engine/web-framework 1.23.0 → 1.24.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/CHANGELOG.md +33 -0
- package/LICENSE +204 -0
- package/README.md +5 -1
- package/bin/build/hash-password.js +12 -3
- package/bin/static/scripts/lib/alpinejs-csp.min.js +3 -3
- package/bin/static/scripts/ti-charts.js +12 -3
- package/bin/static/scripts/ti-framework.js +12 -3
- package/bin/web-app-manager.js +12 -3
- package/bin/web-server.js +15 -3
- package/components/admin-config-handlers.js +43 -3
- package/components/application-info.js +12 -3
- package/components/auth-manager.js +12 -3
- package/components/authorization.js +12 -3
- package/components/config-change-notifier.js +12 -3
- package/components/config-drift.js +150 -0
- package/components/config-registry.js +12 -3
- package/components/config-service.js +109 -3
- package/components/config-store.js +12 -3
- package/components/definitions.types.js +12 -3
- package/components/local-user-directory.js +12 -3
- package/components/session-store.js +12 -3
- package/components/user.js +12 -3
- package/components/web-config-env.js +12 -3
- package/components/web-handlers.js +12 -3
- package/package.json +11 -3
- 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
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const { EventEmitter } = require( "node:events" );
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pure structural diff between a configuration document's file default and the value currently held in the store.
|
|
20
|
+
* <br/>
|
|
21
|
+
* The store seeds from a file default exactly once ({@link ConfigStore#seedIfEmpty}), so a release that changes a
|
|
22
|
+
* config file changes nothing an already-seeded deployment serves. This module is the detection half of the remedy:
|
|
23
|
+
* it answers "how does the shipped default differ from what this deployment is running", in terms legible enough for
|
|
24
|
+
* an admin to judge whether applying it is safe.
|
|
25
|
+
* <br/>
|
|
26
|
+
* No I/O — the caller supplies both values.
|
|
27
|
+
*
|
|
28
|
+
* @module config-drift
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const { isDeepStrictEqual } = require( "node:util" );
|
|
32
|
+
|
|
33
|
+
const STATUS_IN_SYNC = "in-sync";
|
|
34
|
+
const STATUS_DRIFTED = "drifted";
|
|
35
|
+
const STATUS_ABSENT = "absent";
|
|
36
|
+
const STATUS_NO_DEFAULT = "no-default";
|
|
37
|
+
|
|
38
|
+
const KIND_ADDED = "added";
|
|
39
|
+
const KIND_REMOVED = "removed";
|
|
40
|
+
const KIND_CHANGED = "changed";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {Object} ConfigDriftEntry
|
|
44
|
+
* @property {string} path Dot/bracket data path, matching the dialect used for schema validation issues.
|
|
45
|
+
* @property {string} kind One of "added", "removed", "changed".
|
|
46
|
+
* @property {number} [addedMembers] For a primitive array: how many members the file default adds.
|
|
47
|
+
* @property {number} [removedMembers] For a primitive array: how many members the file default drops.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {*} value
|
|
52
|
+
* @returns {boolean} True for a non-null, non-array object.
|
|
53
|
+
*/
|
|
54
|
+
function isPlainObject( value ) {
|
|
55
|
+
return value !== null && typeof value === "object" && !Array.isArray( value );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {*} value
|
|
60
|
+
* @returns {boolean} True for an array holding no objects (a code list, a set of flags, …).
|
|
61
|
+
*/
|
|
62
|
+
function isPrimitiveArray( value ) {
|
|
63
|
+
return Array.isArray( value ) && value.every( ( item ) => item === null || typeof item !== "object" );
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} base
|
|
68
|
+
* @param {string} key
|
|
69
|
+
* @returns {string} The child path, numeric keys in bracket notation.
|
|
70
|
+
*/
|
|
71
|
+
function joinPath( base, key ) {
|
|
72
|
+
return ( /^\d+$/.test( key ) ) ? `${ base }[${ key }]` : `${ base }.${ key }`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursive worker. Appends to `entries` in place.
|
|
77
|
+
*
|
|
78
|
+
* @param {*} fileValue
|
|
79
|
+
* @param {*} storedValue
|
|
80
|
+
* @param {string} path
|
|
81
|
+
* @param {ConfigDriftEntry[]} entries
|
|
82
|
+
*/
|
|
83
|
+
function diffValue( fileValue, storedValue, path, entries ) {
|
|
84
|
+
if ( isPlainObject( fileValue ) && isPlainObject( storedValue ) ) {
|
|
85
|
+
const keys = new Set( [ ...Object.keys( fileValue ), ...Object.keys( storedValue ) ] );
|
|
86
|
+
for ( const key of keys ) {
|
|
87
|
+
const childPath = joinPath( path, key );
|
|
88
|
+
const inFile = Object.prototype.hasOwnProperty.call( fileValue, key );
|
|
89
|
+
const inStored = Object.prototype.hasOwnProperty.call( storedValue, key );
|
|
90
|
+
if ( inFile && !inStored ) {
|
|
91
|
+
entries.push( { path: childPath, kind: KIND_ADDED } );
|
|
92
|
+
} else if ( !inFile && inStored ) {
|
|
93
|
+
entries.push( { path: childPath, kind: KIND_REMOVED } );
|
|
94
|
+
} else {
|
|
95
|
+
diffValue( fileValue[ key ], storedValue[ key ], childPath, entries );
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A list of codes is a set, not a sequence: report which members moved, and treat a pure reorder as no change.
|
|
102
|
+
// This is what turns "role-family-competencies changed" into the far more useful "QE +27 codes".
|
|
103
|
+
// <br/>
|
|
104
|
+
// Set semantics also mean MULTIPLICITY is ignored — `[ "A", "A" ]` and `[ "A" ]` compare as in-sync even though
|
|
105
|
+
// applying would replace one with the other. That is deliberate for a list of codes, where a repeated member is
|
|
106
|
+
// a data error rather than a meaningful difference, and it is unreachable for the documents shipped here: the
|
|
107
|
+
// array-valued competence schemas all declare `uniqueItems`, so a duplicate cannot pass validation on save. A
|
|
108
|
+
// consumer whose arrays are genuinely multisets should not model them as primitive arrays for this diff.
|
|
109
|
+
if ( isPrimitiveArray( fileValue ) && isPrimitiveArray( storedValue ) ) {
|
|
110
|
+
const storedMembers = new Set( storedValue );
|
|
111
|
+
const fileMembers = new Set( fileValue );
|
|
112
|
+
const addedMembers = fileValue.filter( ( item ) => !storedMembers.has( item ) ).length;
|
|
113
|
+
const removedMembers = storedValue.filter( ( item ) => !fileMembers.has( item ) ).length;
|
|
114
|
+
if ( addedMembers > 0 || removedMembers > 0 ) {
|
|
115
|
+
entries.push( { path: path, kind: KIND_CHANGED, addedMembers: addedMembers, removedMembers: removedMembers } );
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if ( !isDeepStrictEqual( fileValue, storedValue ) ) {
|
|
121
|
+
entries.push( { path: path, kind: KIND_CHANGED } );
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Diffs a document's registered file default against its stored value.
|
|
127
|
+
*
|
|
128
|
+
* @method
|
|
129
|
+
* @param {*} fileDefault The value registered with {@link ConfigRegistry#register}; `undefined` when none was.
|
|
130
|
+
* @param {*} storedValue The value currently in the store; `null`/`undefined` when never written.
|
|
131
|
+
* @returns {{status: string, entries: ConfigDriftEntry[], counts: {added: number, removed: number, changed: number}}}
|
|
132
|
+
* @public
|
|
133
|
+
*/
|
|
134
|
+
module.exports.diffDocument = ( fileDefault, storedValue ) => {
|
|
135
|
+
if ( fileDefault === undefined ) {
|
|
136
|
+
return { status: STATUS_NO_DEFAULT, entries: [], counts: { added: 0, removed: 0, changed: 0 } };
|
|
137
|
+
}
|
|
138
|
+
if ( storedValue === undefined || storedValue === null ) {
|
|
139
|
+
return { status: STATUS_ABSENT, entries: [], counts: { added: 0, removed: 0, changed: 0 } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entries = [];
|
|
143
|
+
diffValue( fileDefault, storedValue, "", entries );
|
|
144
|
+
|
|
145
|
+
const counts = { added: 0, removed: 0, changed: 0 };
|
|
146
|
+
for ( const entry of entries ) {
|
|
147
|
+
counts[ entry.kind ] += 1;
|
|
148
|
+
}
|
|
149
|
+
return { status: ( entries.length > 0 ) ? STATUS_DRIFTED : STATUS_IN_SYNC, entries: entries, counts: counts };
|
|
150
|
+
};
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const Ajv = require( "ajv" );
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const exceptions = require( "@ti-engine/core/exceptions" );
|
|
19
|
+
const configDrift = require( "#config-drift" );
|
|
10
20
|
|
|
11
21
|
/** @import ConfigChangeNotifier from "#config-change-notifier" */
|
|
12
22
|
/** @import ConfigRegistry from "#config-registry" */
|
|
@@ -307,6 +317,102 @@ class ConfigService {
|
|
|
307
317
|
} ) );
|
|
308
318
|
}
|
|
309
319
|
|
|
320
|
+
/* Public interface — drift against file defaults */
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compares a document's registered file default against the value currently in the store. This is how a
|
|
324
|
+
* configuration change shipped in a release becomes visible on a deployment that was seeded before it — the
|
|
325
|
+
* store seeds only once, so a later file change is otherwise invisible.
|
|
326
|
+
*
|
|
327
|
+
* @method
|
|
328
|
+
* @param {string} configKey
|
|
329
|
+
* @returns {Promise<{configKey: string, status: string, counts: Object, entries: Array, storedVersion: number, editable: boolean, label: string}>}
|
|
330
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} If the document is not registered.
|
|
331
|
+
* @public
|
|
332
|
+
*/
|
|
333
|
+
getDrift( configKey ) {
|
|
334
|
+
if ( !this.#registry.has( configKey ) ) {
|
|
335
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKey: configKey } ) );
|
|
336
|
+
}
|
|
337
|
+
const metadata = this.#registry.metadataFor( configKey ) || {};
|
|
338
|
+
return this.#store.getCurrent( configKey ).then( ( current ) => {
|
|
339
|
+
const diff = configDrift.diffDocument( this.#registry.getDefault( configKey ), current ? current.value : null );
|
|
340
|
+
return {
|
|
341
|
+
configKey: configKey,
|
|
342
|
+
status: diff.status,
|
|
343
|
+
counts: diff.counts,
|
|
344
|
+
entries: diff.entries,
|
|
345
|
+
storedVersion: current ? current.version : 0,
|
|
346
|
+
editable: metadata.editable !== false,
|
|
347
|
+
label: metadata.label || configKey
|
|
348
|
+
};
|
|
349
|
+
} );
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Drift summaries for every registered document. This still computes each document's full entry list internally
|
|
354
|
+
* (it delegates to {@link ConfigService#getDrift} per document) — the saving is in the response shape, not the
|
|
355
|
+
* computation: `entries` is omitted here to keep the payload small enough for a landing screen and a startup log,
|
|
356
|
+
* where only the counts are shown.
|
|
357
|
+
*
|
|
358
|
+
* @method
|
|
359
|
+
* @returns {Promise<Array<Object>>}
|
|
360
|
+
* @public
|
|
361
|
+
*/
|
|
362
|
+
listDrift() {
|
|
363
|
+
return Promise.all( this.#registry.list().map( ( configKey ) => {
|
|
364
|
+
return this.getDrift( configKey ).then( ( drift ) => ( {
|
|
365
|
+
configKey: drift.configKey,
|
|
366
|
+
status: drift.status,
|
|
367
|
+
counts: drift.counts,
|
|
368
|
+
storedVersion: drift.storedVersion,
|
|
369
|
+
editable: drift.editable,
|
|
370
|
+
label: drift.label
|
|
371
|
+
} ) );
|
|
372
|
+
} ) );
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Applies the registered file defaults for the given documents, as a single validated change-set.
|
|
377
|
+
* <br/>
|
|
378
|
+
* Routing through {@link ConfigService#applyEdits} is deliberate: the application is schema- and
|
|
379
|
+
* semantically validated, versioned, correlated into one change-set, added to the audit feed, and restorable —
|
|
380
|
+
* and, because a validator sees its siblings at their *pending* value, interdependent documents applied
|
|
381
|
+
* together validate against each other rather than against the stale stored state.
|
|
382
|
+
*
|
|
383
|
+
* @method
|
|
384
|
+
* @param {string[]} configKeys
|
|
385
|
+
* @param {Object} meta
|
|
386
|
+
* @param {string} meta.adminID
|
|
387
|
+
* @param {string} [meta.note]
|
|
388
|
+
* @returns {Promise<{ok: true, changeSetID: string, versions: Object}|{ok: false, errors: Object}>}
|
|
389
|
+
* @throws {TiException.E_WEB_INVALID_REQUEST_PARAMETERS} On bad input, an unknown key, or a key with no default.
|
|
390
|
+
* @public
|
|
391
|
+
*/
|
|
392
|
+
applyDefaults( configKeys, meta ) {
|
|
393
|
+
if ( !Array.isArray( configKeys ) || configKeys.length === 0 || !meta || !meta.adminID ) {
|
|
394
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "invalid-apply-defaults-input" } ) );
|
|
395
|
+
}
|
|
396
|
+
const keys = Array.from( new Set( configKeys ) );
|
|
397
|
+
const unknown = keys.filter( ( key ) => !this.#registry.has( key ) );
|
|
398
|
+
if ( unknown.length > 0 ) {
|
|
399
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "unknown-config", configKeys: unknown } ) );
|
|
400
|
+
}
|
|
401
|
+
const withoutDefault = keys.filter( ( key ) => this.#registry.getDefault( key ) === undefined );
|
|
402
|
+
if ( withoutDefault.length > 0 ) {
|
|
403
|
+
return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_WEB_INVALID_REQUEST_PARAMETERS, { reason: "no-default", configKeys: withoutDefault } ) );
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return Promise.all( keys.map( ( key ) => this.#store.getCurrent( key ) ) ).then( ( currents ) => {
|
|
407
|
+
const edits = keys.map( ( key, index ) => ( {
|
|
408
|
+
configKey: key,
|
|
409
|
+
value: this.#registry.getDefault( key ),
|
|
410
|
+
expectedVersion: currents[ index ] ? currents[ index ].version : 0
|
|
411
|
+
} ) );
|
|
412
|
+
return this.applyEdits( edits, { adminID: meta.adminID, note: meta.note || "applied file defaults" } );
|
|
413
|
+
} );
|
|
414
|
+
}
|
|
415
|
+
|
|
310
416
|
/**
|
|
311
417
|
* Seeds a document's default value into the store only if it has never been written (idempotent bootstrap).
|
|
312
418
|
* Used by an application to bring its file defaults into the store at startup before serving live config.
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const cache = require( "@ti-engine/core/cache" );
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
/** @import { TiLocalizationLanguage } from "@ti-engine/core/localization" */
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const crypto = require( "node:crypto" );
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const cache = require( "@ti-engine/core/cache" );
|
package/components/user.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
/** @import { TiLocalizationLanguage } from "@ti-engine/core/localization" */
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
"use strict";
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/*
|
|
2
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
3
|
* Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
const logger = require( "@ti-engine/core/logger" );
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/web-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
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",
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
"csp"
|
|
14
14
|
],
|
|
15
15
|
"author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
|
|
16
|
-
"license": "
|
|
16
|
+
"license": "Apache-2.0",
|
|
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"
|
|
@@ -108,7 +116,7 @@
|
|
|
108
116
|
"@ti-engine/core": "*",
|
|
109
117
|
"@types/express": "^5.0.6",
|
|
110
118
|
"@types/express-session": "^1.18.2",
|
|
111
|
-
"@types/node": "^
|
|
119
|
+
"@types/node": "^26.2.0",
|
|
112
120
|
"ajv": "^8.20.0",
|
|
113
121
|
"cookie-parser": "^1.4.7",
|
|
114
122
|
"express": "^5.2.1",
|
|
@@ -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.
|