@dudousxd/nestjs-catalog 0.9.0 → 0.11.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/dist/catalog.controller.js +39 -11
- package/dist/catalog.events.d.ts +79 -8
- package/dist/catalog.events.js +40 -1
- package/dist/catalog.overlay-store.d.ts +29 -0
- package/dist/catalog.overlay-store.js +45 -2
- package/dist/catalog.pipeline.d.ts +82 -2
- package/dist/catalog.pipeline.js +60 -2
- package/dist/catalog.registry.base.d.ts +15 -4
- package/dist/catalog.registry.d.ts +9 -3
- package/dist/catalog.registry.js +22 -4
- package/dist/catalog.secrets.d.ts +283 -0
- package/dist/catalog.secrets.js +209 -0
- package/dist/catalog.service.d.ts +20 -5
- package/dist/catalog.service.js +23 -8
- package/dist/client.d.ts +2 -1
- package/dist/client.js +9 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +14 -2
- package/dist/transform-runner.d.ts +22 -5
- package/dist/transform-runner.js +126 -9
- package/package.json +1 -1
|
@@ -148,24 +148,44 @@ function createCatalogController(path, guards, decorators = []) {
|
|
|
148
148
|
/**
|
|
149
149
|
* Tier 0. Renames a type, regroups it, changes its icon. No migration, no
|
|
150
150
|
* deploy, no engineer.
|
|
151
|
+
*
|
|
152
|
+
* The principal is passed for the reason the sharing routes below pass one:
|
|
153
|
+
* a curated label outlives the publisher's next deploy, so "who renamed this
|
|
154
|
+
* column" is asked long after any log has rotated. `actorOf` and not
|
|
155
|
+
* `request.principal.id` directly, so an unguarded mount records `console`
|
|
156
|
+
* rather than an empty actor — see the note on it.
|
|
157
|
+
*
|
|
158
|
+
* The body deliberately names no `createdBy`-style override, unlike the
|
|
159
|
+
* saved-query create route. Nothing here stores an author; the only consumer
|
|
160
|
+
* of the name is the audit entry, and a caller that can write any string into
|
|
161
|
+
* the trail's actor column is worse than one recorded as the console.
|
|
151
162
|
*/
|
|
152
|
-
async patchType(name, body) {
|
|
153
|
-
const updated = await this.registry.patchType(name, body);
|
|
163
|
+
async patchType(name, body, request) {
|
|
164
|
+
const updated = await this.registry.patchType(name, body, actorOf(request));
|
|
154
165
|
if (!updated)
|
|
155
166
|
throw new common_1.NotFoundException(`Unknown object type: ${name}`);
|
|
156
167
|
return updated;
|
|
157
168
|
}
|
|
158
169
|
/** Tier 0, one property at a time. */
|
|
159
|
-
async patchProperty(name, property, body) {
|
|
160
|
-
const updated = await this.registry.patchProperty(name, property, body);
|
|
170
|
+
async patchProperty(name, property, body, request) {
|
|
171
|
+
const updated = await this.registry.patchProperty(name, property, body, actorOf(request));
|
|
161
172
|
if (!updated) {
|
|
162
173
|
throw new common_1.NotFoundException(`Unknown property: ${name}.${property}`);
|
|
163
174
|
}
|
|
164
175
|
return updated;
|
|
165
176
|
}
|
|
166
|
-
/**
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Drops every tier-0 edit and falls back to what the ORM says.
|
|
179
|
+
*
|
|
180
|
+
* The actor matters most here of the three. This destroys every curated
|
|
181
|
+
* label, unit and **classification** in the catalog in one request, under the
|
|
182
|
+
* same `catalog:curate` a rename needs — and un-classifying a property
|
|
183
|
+
* re-admits its name to searches by principals who could not see it an
|
|
184
|
+
* instant earlier. Nothing versions the overlay, so after this the only
|
|
185
|
+
* record that it happened, and of who did it, is the event.
|
|
186
|
+
*/
|
|
187
|
+
async reset(request) {
|
|
188
|
+
await this.registry.resetOverlay(actorOf(request));
|
|
169
189
|
return this.registry.getSnapshot();
|
|
170
190
|
}
|
|
171
191
|
/** One generic read endpoint for every type in the catalog. */
|
|
@@ -414,8 +434,9 @@ function createCatalogController(path, guards, decorators = []) {
|
|
|
414
434
|
(0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
|
|
415
435
|
__param(0, (0, common_1.Param)('name')),
|
|
416
436
|
__param(1, (0, common_1.Body)()),
|
|
437
|
+
__param(2, (0, common_1.Req)()),
|
|
417
438
|
__metadata("design:type", Function),
|
|
418
|
-
__metadata("design:paramtypes", [String, Object]),
|
|
439
|
+
__metadata("design:paramtypes", [String, Object, Object]),
|
|
419
440
|
__metadata("design:returntype", Promise)
|
|
420
441
|
], CatalogController.prototype, "patchType", null);
|
|
421
442
|
__decorate([
|
|
@@ -424,15 +445,17 @@ function createCatalogController(path, guards, decorators = []) {
|
|
|
424
445
|
__param(0, (0, common_1.Param)('name')),
|
|
425
446
|
__param(1, (0, common_1.Param)('property')),
|
|
426
447
|
__param(2, (0, common_1.Body)()),
|
|
448
|
+
__param(3, (0, common_1.Req)()),
|
|
427
449
|
__metadata("design:type", Function),
|
|
428
|
-
__metadata("design:paramtypes", [String, String, Object]),
|
|
450
|
+
__metadata("design:paramtypes", [String, String, Object, Object]),
|
|
429
451
|
__metadata("design:returntype", Promise)
|
|
430
452
|
], CatalogController.prototype, "patchProperty", null);
|
|
431
453
|
__decorate([
|
|
432
454
|
(0, common_1.Post)('reset'),
|
|
433
455
|
(0, catalog_route_auth_1.RequireScopes)('catalog:curate'),
|
|
456
|
+
__param(0, (0, common_1.Req)()),
|
|
434
457
|
__metadata("design:type", Function),
|
|
435
|
-
__metadata("design:paramtypes", []),
|
|
458
|
+
__metadata("design:paramtypes", [Object]),
|
|
436
459
|
__metadata("design:returntype", Promise)
|
|
437
460
|
], CatalogController.prototype, "reset", null);
|
|
438
461
|
__decorate([
|
|
@@ -656,7 +679,12 @@ function createCatalogController(path, guards, decorators = []) {
|
|
|
656
679
|
return CatalogController;
|
|
657
680
|
}
|
|
658
681
|
/**
|
|
659
|
-
* Who to record a workspace
|
|
682
|
+
* Who to record a change against — a workspace one, and now a curation one.
|
|
683
|
+
*
|
|
684
|
+
* Shared by both on purpose. The two halves of the trail used to answer the "who"
|
|
685
|
+
* question differently: sharing named its principal and curation named nobody at
|
|
686
|
+
* all, which reads as a bug in whichever half you look at second. One helper is
|
|
687
|
+
* what keeps the two from drifting again, including on the fallback below.
|
|
660
688
|
*
|
|
661
689
|
* The host's resolved principal wins over anything the body claimed, and that
|
|
662
690
|
* order is the whole point: a `createdBy` in a request body is a name the caller
|
package/dist/catalog.events.d.ts
CHANGED
|
@@ -44,6 +44,41 @@ export declare const CATALOG_EVENT_PHASE: Record<CatalogEvent, number>;
|
|
|
44
44
|
*/
|
|
45
45
|
export declare const CATALOG_EVENT_PHASE_FALLBACK = 4;
|
|
46
46
|
export declare function catalogEventPhase(event: string): number;
|
|
47
|
+
/**
|
|
48
|
+
* What a curation entry says when nothing told it who.
|
|
49
|
+
*
|
|
50
|
+
* A value rather than an empty string or a missing key, and that difference is
|
|
51
|
+
* the whole reason it exists. The shipped recorder writes
|
|
52
|
+
* `principalId: undefined` for anything falsy, which lands as NULL in the column
|
|
53
|
+
* every governance query filters on — and a NULL there is indistinguishable from
|
|
54
|
+
* the rows written before this library recorded actors at all. "Not captured"
|
|
55
|
+
* and "nobody did this" are different statements, and only the first one is true.
|
|
56
|
+
*
|
|
57
|
+
* Deliberately not the same string as the controller's `console` fallback, which
|
|
58
|
+
* is a narrower and more useful claim: `console` says a request came through
|
|
59
|
+
* this library's own HTTP surface and no guard resolved a principal onto it —
|
|
60
|
+
* the deployment has an unauthenticated mount. This one says the registry API
|
|
61
|
+
* was called in-process and the caller named nobody: a host script, a scheduled
|
|
62
|
+
* job, or a subclass compiled against the signature before it took an actor.
|
|
63
|
+
* Collapsing them would throw away the only clue about where to go looking, in
|
|
64
|
+
* exchange for one fewer constant.
|
|
65
|
+
*/
|
|
66
|
+
export declare const UNATTRIBUTED_PRINCIPAL_ID = "unattributed";
|
|
67
|
+
/**
|
|
68
|
+
* The actor a curation event will carry, given whatever the caller passed.
|
|
69
|
+
*
|
|
70
|
+
* Exported because both registries need it and they ship in different packages —
|
|
71
|
+
* the in-app one here, the stored one in `store-mikro-orm` — so a copy each is a
|
|
72
|
+
* rule that holds in two places right up until it holds in one.
|
|
73
|
+
*
|
|
74
|
+
* Total, and it re-checks a parameter the types already made required. That is
|
|
75
|
+
* not belt-and-braces: `CatalogRegistry` binds TypeScript callers, and the
|
|
76
|
+
* callers whose omission must never reach the trail are precisely the ones it
|
|
77
|
+
* does not bind — a JavaScript host, and a subclass declaring the older
|
|
78
|
+
* argument list, which stays a legal override because TypeScript lets an
|
|
79
|
+
* implementation take fewer parameters than it promised.
|
|
80
|
+
*/
|
|
81
|
+
export declare function curationActor(principalId: string | undefined): string;
|
|
47
82
|
export interface CatalogEventPayloads {
|
|
48
83
|
/** DDL was applied to an object type's physical table. Always additive. */
|
|
49
84
|
'schema.changed': {
|
|
@@ -80,6 +115,38 @@ export interface CatalogEventPayloads {
|
|
|
80
115
|
typeName: string;
|
|
81
116
|
property?: string;
|
|
82
117
|
changed: string[];
|
|
118
|
+
/**
|
|
119
|
+
* Who renamed it — the half of that sentence this payload used to leave out.
|
|
120
|
+
*
|
|
121
|
+
* It carried `typeName`, `property` and `changed`, which answers "what" and
|
|
122
|
+
* (with the row's timestamp) "when", and never "who" — while `query.shared`
|
|
123
|
+
* two screens away named its actor from the day it was added. An audit trail
|
|
124
|
+
* that is inconsistent about attribution reads as broken in whichever half
|
|
125
|
+
* you look at second, and curation is the side where it matters more:
|
|
126
|
+
* a curated label is the one decision this library describes as surviving
|
|
127
|
+
* the publisher's next deploy.
|
|
128
|
+
*
|
|
129
|
+
* **`principalId`, not `curatedBy`,** because the spelling is a contract with
|
|
130
|
+
* a recorder this package cannot import. `CatalogAuditRecorder` lifts exactly
|
|
131
|
+
* this key into the audit table's indexed column; a payload that names it
|
|
132
|
+
* anything else still carries the actor, in a JSON blob no query anybody runs
|
|
133
|
+
* will look inside, and the entry lands attributed to nobody while looking
|
|
134
|
+
* complete.
|
|
135
|
+
*
|
|
136
|
+
* **The whole `CatalogPrincipal.id`, composite half included.** The same
|
|
137
|
+
* choice `query.shared` made, for the reason `catalog.principal.ts` argues at
|
|
138
|
+
* length: `parsePrincipalId` recovers the application from an
|
|
139
|
+
* `<app>#<person>` id, so carrying the person costs the machine-level
|
|
140
|
+
* question nothing — while dropping to `applicationId` would file a curator's
|
|
141
|
+
* decision under the console they happened to sign into, and "the console
|
|
142
|
+
* renamed this column" is the answer that file says nobody accepts.
|
|
143
|
+
*
|
|
144
|
+
* **Required, and never the empty string.** The recorder treats a falsy value
|
|
145
|
+
* as absent and writes NULL, which reads as "nobody did this" rather than
|
|
146
|
+
* "this was not captured". A producer holding no principal emits
|
|
147
|
+
* {@link UNATTRIBUTED_PRINCIPAL_ID} instead, which is a statement.
|
|
148
|
+
*/
|
|
149
|
+
principalId: string;
|
|
83
150
|
};
|
|
84
151
|
/**
|
|
85
152
|
* The whole overlay was discarded — every curated label, description, unit,
|
|
@@ -123,20 +190,24 @@ export interface CatalogEventPayloads {
|
|
|
123
190
|
* to whom, they are a small subset of it, and re-typing them is the only
|
|
124
191
|
* recovery anybody can perform.
|
|
125
192
|
*
|
|
126
|
-
* **
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
193
|
+
* **It carries `principalId`, which it did not at first**, and the reason the
|
|
194
|
+
* gap existed is worth keeping: `resetOverlay()` took no principal, the route
|
|
195
|
+
* resolved none for it, and `RoutingCatalogRegistry` forwards the call by hand,
|
|
196
|
+
* so a field here would have been empty on every row — and an audit table
|
|
197
|
+
* lifts `principalId` into a column where empty reads as "nobody did this"
|
|
198
|
+
* rather than "this was not captured". The answer was to thread the actor
|
|
199
|
+
* through all three rather than to keep documenting its absence, because this
|
|
200
|
+
* is the one act on the catalog that destroys decisions in bulk and needs only
|
|
201
|
+
* `catalog:curate` to do it. See `type.curated` above for what the field holds
|
|
202
|
+
* and why it is spelled that way.
|
|
134
203
|
*
|
|
135
204
|
* Emitted even when the overlay was empty, with zeroes. A trail that recorded
|
|
136
205
|
* only destructive resets cannot tell "nobody pressed it" from "somebody
|
|
137
206
|
* pressed it and nothing was there", and the second is worth seeing.
|
|
138
207
|
*/
|
|
139
208
|
'overlay.reset': {
|
|
209
|
+
/** Who reverted the catalog. See `type.curated`'s `principalId`. */
|
|
210
|
+
principalId: string;
|
|
140
211
|
/** Every type that carried curation, so the trail names what was lost. */
|
|
141
212
|
typeNames: string[];
|
|
142
213
|
/** How many per-property entries went with them, across every type. */
|
package/dist/catalog.events.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CATALOG_EVENTS = exports.CATALOG_LIB = void 0;
|
|
3
|
+
exports.UNATTRIBUTED_PRINCIPAL_ID = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CATALOG_EVENTS = exports.CATALOG_LIB = void 0;
|
|
4
4
|
exports.catalogEventPhase = catalogEventPhase;
|
|
5
|
+
exports.curationActor = curationActor;
|
|
5
6
|
exports.channelNameFor = channelNameFor;
|
|
6
7
|
exports.emitCatalog = emitCatalog;
|
|
7
8
|
const nestjs_diagnostics_1 = require("@dudousxd/nestjs-diagnostics");
|
|
@@ -102,6 +103,44 @@ function catalogEventPhase(event) {
|
|
|
102
103
|
const phase = Reflect.get(exports.CATALOG_EVENT_PHASE, event);
|
|
103
104
|
return typeof phase === 'number' ? phase : exports.CATALOG_EVENT_PHASE_FALLBACK;
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* What a curation entry says when nothing told it who.
|
|
108
|
+
*
|
|
109
|
+
* A value rather than an empty string or a missing key, and that difference is
|
|
110
|
+
* the whole reason it exists. The shipped recorder writes
|
|
111
|
+
* `principalId: undefined` for anything falsy, which lands as NULL in the column
|
|
112
|
+
* every governance query filters on — and a NULL there is indistinguishable from
|
|
113
|
+
* the rows written before this library recorded actors at all. "Not captured"
|
|
114
|
+
* and "nobody did this" are different statements, and only the first one is true.
|
|
115
|
+
*
|
|
116
|
+
* Deliberately not the same string as the controller's `console` fallback, which
|
|
117
|
+
* is a narrower and more useful claim: `console` says a request came through
|
|
118
|
+
* this library's own HTTP surface and no guard resolved a principal onto it —
|
|
119
|
+
* the deployment has an unauthenticated mount. This one says the registry API
|
|
120
|
+
* was called in-process and the caller named nobody: a host script, a scheduled
|
|
121
|
+
* job, or a subclass compiled against the signature before it took an actor.
|
|
122
|
+
* Collapsing them would throw away the only clue about where to go looking, in
|
|
123
|
+
* exchange for one fewer constant.
|
|
124
|
+
*/
|
|
125
|
+
exports.UNATTRIBUTED_PRINCIPAL_ID = 'unattributed';
|
|
126
|
+
/**
|
|
127
|
+
* The actor a curation event will carry, given whatever the caller passed.
|
|
128
|
+
*
|
|
129
|
+
* Exported because both registries need it and they ship in different packages —
|
|
130
|
+
* the in-app one here, the stored one in `store-mikro-orm` — so a copy each is a
|
|
131
|
+
* rule that holds in two places right up until it holds in one.
|
|
132
|
+
*
|
|
133
|
+
* Total, and it re-checks a parameter the types already made required. That is
|
|
134
|
+
* not belt-and-braces: `CatalogRegistry` binds TypeScript callers, and the
|
|
135
|
+
* callers whose omission must never reach the trail are precisely the ones it
|
|
136
|
+
* does not bind — a JavaScript host, and a subclass declaring the older
|
|
137
|
+
* argument list, which stays a legal override because TypeScript lets an
|
|
138
|
+
* implementation take fewer parameters than it promised.
|
|
139
|
+
*/
|
|
140
|
+
function curationActor(principalId) {
|
|
141
|
+
const trimmed = typeof principalId === 'string' ? principalId.trim() : '';
|
|
142
|
+
return trimmed.length > 0 ? trimmed : exports.UNATTRIBUTED_PRINCIPAL_ID;
|
|
143
|
+
}
|
|
105
144
|
/**
|
|
106
145
|
* The channel an event is published on.
|
|
107
146
|
*
|
|
@@ -38,6 +38,35 @@ export declare class FileCatalogOverlayStore implements CatalogOverlayStore {
|
|
|
38
38
|
* turn a policy answer ("you may not curate here") into a 500 from a route the
|
|
39
39
|
* library documents as working, and would put a second mechanism beside the one
|
|
40
40
|
* that already decides.
|
|
41
|
+
*
|
|
42
|
+
* **Both ends copy, and one end would not have been enough.** The registry holds
|
|
43
|
+
* the overlay it loaded and edits it in place — `this.overlay.types[name] = {
|
|
44
|
+
* ...current, ...patch }` — before calling `save`. Copy only on `load` and the
|
|
45
|
+
* object handed to `save` becomes the store's own, so the next patch is writing
|
|
46
|
+
* into the store again; copy only on `save` and the object handed out by `load`
|
|
47
|
+
* already is the store's own. Either way this store's state moves before
|
|
48
|
+
* anybody asked it to, and "nothing is stored until save" — the one sentence a
|
|
49
|
+
* store is for — is not true of it.
|
|
50
|
+
*
|
|
51
|
+
* That mattered in two directions, neither of them the net behaviour, which was
|
|
52
|
+
* and is identical because every edit is followed by a persist.
|
|
53
|
+
*
|
|
54
|
+
* - **The two bundled stores disagreed.** {@link FileCatalogOverlayStore}
|
|
55
|
+
* round-trips through JSON and so has never aliased anything. Every spec in
|
|
56
|
+
* this repository runs on this one, so a test asserting that an edit had not
|
|
57
|
+
* been written yet passed here and would have failed on the store a
|
|
58
|
+
* deployment actually uses. A vacuous pass is worse than no test: it is a
|
|
59
|
+
* claim with evidence attached to it.
|
|
60
|
+
* - **Two registries over one store shared mutable state.** One would see the
|
|
61
|
+
* other's half-applied edit with no write between them, which is the shape
|
|
62
|
+
* that produces a report nobody can reproduce.
|
|
63
|
+
*
|
|
64
|
+
* **What it costs.** One deep copy per load and per save. The overlay is the
|
|
65
|
+
* names, descriptions and per-property patches a human has typed — not the
|
|
66
|
+
* catalog, which is derived from entity metadata and does not live here — so a
|
|
67
|
+
* heavily curated thousand-type catalog is a few hundred kilobytes and a copy
|
|
68
|
+
* in the low milliseconds. The two paths that pay it are a boot and a curator
|
|
69
|
+
* pressing save. Neither is a read, and nothing on a request path calls either.
|
|
41
70
|
*/
|
|
42
71
|
export declare class InMemoryCatalogOverlayStore implements CatalogOverlayStore {
|
|
43
72
|
private overlay;
|
|
@@ -55,17 +55,60 @@ exports.FileCatalogOverlayStore = FileCatalogOverlayStore;
|
|
|
55
55
|
* turn a policy answer ("you may not curate here") into a 500 from a route the
|
|
56
56
|
* library documents as working, and would put a second mechanism beside the one
|
|
57
57
|
* that already decides.
|
|
58
|
+
*
|
|
59
|
+
* **Both ends copy, and one end would not have been enough.** The registry holds
|
|
60
|
+
* the overlay it loaded and edits it in place — `this.overlay.types[name] = {
|
|
61
|
+
* ...current, ...patch }` — before calling `save`. Copy only on `load` and the
|
|
62
|
+
* object handed to `save` becomes the store's own, so the next patch is writing
|
|
63
|
+
* into the store again; copy only on `save` and the object handed out by `load`
|
|
64
|
+
* already is the store's own. Either way this store's state moves before
|
|
65
|
+
* anybody asked it to, and "nothing is stored until save" — the one sentence a
|
|
66
|
+
* store is for — is not true of it.
|
|
67
|
+
*
|
|
68
|
+
* That mattered in two directions, neither of them the net behaviour, which was
|
|
69
|
+
* and is identical because every edit is followed by a persist.
|
|
70
|
+
*
|
|
71
|
+
* - **The two bundled stores disagreed.** {@link FileCatalogOverlayStore}
|
|
72
|
+
* round-trips through JSON and so has never aliased anything. Every spec in
|
|
73
|
+
* this repository runs on this one, so a test asserting that an edit had not
|
|
74
|
+
* been written yet passed here and would have failed on the store a
|
|
75
|
+
* deployment actually uses. A vacuous pass is worse than no test: it is a
|
|
76
|
+
* claim with evidence attached to it.
|
|
77
|
+
* - **Two registries over one store shared mutable state.** One would see the
|
|
78
|
+
* other's half-applied edit with no write between them, which is the shape
|
|
79
|
+
* that produces a report nobody can reproduce.
|
|
80
|
+
*
|
|
81
|
+
* **What it costs.** One deep copy per load and per save. The overlay is the
|
|
82
|
+
* names, descriptions and per-property patches a human has typed — not the
|
|
83
|
+
* catalog, which is derived from entity metadata and does not live here — so a
|
|
84
|
+
* heavily curated thousand-type catalog is a few hundred kilobytes and a copy
|
|
85
|
+
* in the low milliseconds. The two paths that pay it are a boot and a curator
|
|
86
|
+
* pressing save. Neither is a read, and nothing on a request path calls either.
|
|
58
87
|
*/
|
|
59
88
|
class InMemoryCatalogOverlayStore {
|
|
60
89
|
overlay = { types: {} };
|
|
61
90
|
async load() {
|
|
62
|
-
return this.overlay;
|
|
91
|
+
return copyOverlay(this.overlay);
|
|
63
92
|
}
|
|
64
93
|
async save(overlay) {
|
|
65
|
-
this.overlay = overlay;
|
|
94
|
+
this.overlay = copyOverlay(overlay);
|
|
66
95
|
}
|
|
67
96
|
}
|
|
68
97
|
exports.InMemoryCatalogOverlayStore = InMemoryCatalogOverlayStore;
|
|
98
|
+
/**
|
|
99
|
+
* A deep copy, so a caller and the store never hold one object between them.
|
|
100
|
+
*
|
|
101
|
+
* `structuredClone` rather than a `JSON.parse(JSON.stringify(...))` round-trip,
|
|
102
|
+
* which differs on a key whose value is `undefined`: JSON drops it, so
|
|
103
|
+
* `{ displayName: undefined }` comes back as `{}` and `'displayName' in entry`
|
|
104
|
+
* flips from true to false. Nothing reads the overlay that way today. A copy
|
|
105
|
+
* that quietly edits what it copies is still not a copy, and the day something
|
|
106
|
+
* does read it that way the difference is a curated field that vanished with no
|
|
107
|
+
* write behind it.
|
|
108
|
+
*/
|
|
109
|
+
function copyOverlay(overlay) {
|
|
110
|
+
return structuredClone(overlay);
|
|
111
|
+
}
|
|
69
112
|
/**
|
|
70
113
|
* Whether a parsed file is an overlay, checked to the depth that matters.
|
|
71
114
|
*
|
|
@@ -442,13 +442,48 @@ export interface WorkflowGraph {
|
|
|
442
442
|
* an audit. The limitation is real and worth stating plainly — an edited graph
|
|
443
443
|
* cannot be reconstructed from an old run, only identified as different.
|
|
444
444
|
*/
|
|
445
|
+
/**
|
|
446
|
+
* Whether this graph is still being drawn, or is something somebody declared
|
|
447
|
+
* finished.
|
|
448
|
+
*
|
|
449
|
+
* The distinction exists because validation used to be the gate on *saving*, and
|
|
450
|
+
* that made an unfinished graph unstorable: `saveWorkflow` refused anything
|
|
451
|
+
* `validateWorkflow` had an issue with, so a canvas with one node on it could
|
|
452
|
+
* not be written down at all and closing the tab lost it. Worse, it made the
|
|
453
|
+
* canvas lie about ordinary work — clicking "+ Sink" produces a node that is
|
|
454
|
+
* unreachable from any source and names no type, both true and both useless one
|
|
455
|
+
* second after the click, because a just-added node is unwired by construction.
|
|
456
|
+
*
|
|
457
|
+
* So the gate moved rather than loosened. Validation is now the gate on
|
|
458
|
+
* publishing, and the same `validateWorkflow` still decides — a draft is not a
|
|
459
|
+
* graph that skipped the rules, it is a graph nobody has claimed is finished
|
|
460
|
+
* yet. Everything that consumes a workflow asks for `ready`: a connector may
|
|
461
|
+
* only point at one, and a promotion may only carry one. What crosses an
|
|
462
|
+
* environment should be something a person declared done.
|
|
463
|
+
*/
|
|
464
|
+
export declare const WORKFLOW_STATUSES: readonly ["draft", "ready"];
|
|
465
|
+
export type WorkflowStatus = (typeof WORKFLOW_STATUSES)[number];
|
|
466
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
467
|
+
export declare function isWorkflowStatus(value: unknown): value is WorkflowStatus;
|
|
445
468
|
export interface CatalogWorkflow {
|
|
446
469
|
id: string;
|
|
447
470
|
name: string;
|
|
448
471
|
description?: string;
|
|
449
472
|
nodes: WorkflowNode[];
|
|
450
473
|
edges: WorkflowEdge[];
|
|
451
|
-
/**
|
|
474
|
+
/** See {@link WORKFLOW_STATUSES}. A graph is `draft` until somebody publishes it. */
|
|
475
|
+
status: WorkflowStatus;
|
|
476
|
+
/**
|
|
477
|
+
* Bumped whenever the graph's behaviour changes. Never on a rename or a move.
|
|
478
|
+
*
|
|
479
|
+
* Bumped on a **draft** edit too, which looks like exactly the inflation this
|
|
480
|
+
* rule exists to prevent and is not. The counter's job is to make
|
|
481
|
+
* {@link ConnectorRun.workflowVersion} answer "which shape ran": freezing it
|
|
482
|
+
* while a graph is drafted would let a run recorded at v4 and a later run also
|
|
483
|
+
* at v4 mean two different graphs, which is the one thing that field must
|
|
484
|
+
* never do. Drafting therefore inflates a number nobody reads — cheap — rather
|
|
485
|
+
* than making a number somebody does read ambiguous.
|
|
486
|
+
*/
|
|
452
487
|
version: number;
|
|
453
488
|
/** Fingerprint of the graph at this version. See {@link workflowGraphHash}. */
|
|
454
489
|
graphHash: string;
|
|
@@ -660,7 +695,24 @@ export interface CatalogWorkflowStore {
|
|
|
660
695
|
listWorkflows(): Promise<CatalogWorkflow[]>;
|
|
661
696
|
getWorkflow(id: string): Promise<CatalogWorkflow | undefined>;
|
|
662
697
|
/**
|
|
663
|
-
* Validates
|
|
698
|
+
* Writes. Validates only what it must.
|
|
699
|
+
*
|
|
700
|
+
* A **draft** is written without validating, which is the whole of the change
|
|
701
|
+
* and the reason {@link WORKFLOW_STATUSES} exists: a graph you have not
|
|
702
|
+
* finished has to be storable, or closing the tab loses it. A **ready**
|
|
703
|
+
* workflow is still validated on every save, because it is the one that runs.
|
|
704
|
+
*
|
|
705
|
+
* `status` is not an input. A save cannot promote a draft to ready — that is
|
|
706
|
+
* {@link publishWorkflow}, which exists so there is one place that validates
|
|
707
|
+
* and one place that can explain why it refused. A save of an already-ready
|
|
708
|
+
* workflow keeps it ready, and **refuses an edit that would make it invalid**
|
|
709
|
+
* rather than quietly demoting it to draft. Demotion was the other option and
|
|
710
|
+
* it is the one that loses a running pipeline silently: a connector may only
|
|
711
|
+
* point at a ready graph, so a save that dropped the status would disable a
|
|
712
|
+
* scheduled load with nothing said to anybody. Refusing puts the error in
|
|
713
|
+
* front of the person who is editing, at the moment they edit. To park a
|
|
714
|
+
* broken idea on a live graph, {@link unpublishWorkflow} it first and be told
|
|
715
|
+
* which connectors that stops.
|
|
664
716
|
*
|
|
665
717
|
* `version`, `graphHash` and `targetType` are not inputs: the first two are
|
|
666
718
|
* derived from the graph and the third from the sink, and accepting them from
|
|
@@ -670,6 +722,34 @@ export interface CatalogWorkflowStore {
|
|
|
670
722
|
id?: string;
|
|
671
723
|
description?: string;
|
|
672
724
|
}, createdBy: string): Promise<CatalogWorkflow>;
|
|
725
|
+
/**
|
|
726
|
+
* Declare a graph finished: validate it, and make it `ready`.
|
|
727
|
+
*
|
|
728
|
+
* A transition rather than a field on save, and the argument is that this is
|
|
729
|
+
* the only shape with somewhere to put the refusal. "Ready" is a claim that
|
|
730
|
+
* has to be checked, and a check that fails owes an explanation naming the
|
|
731
|
+
* nodes — `validateWorkflow` produces exactly that, and a boolean field on a
|
|
732
|
+
* save request has nowhere to return it that is not an error on an operation
|
|
733
|
+
* the caller thought was about something else. It also makes the audit
|
|
734
|
+
* question answerable: publishing is an act with an actor, and a field set in
|
|
735
|
+
* passing during an autosave is not.
|
|
736
|
+
*
|
|
737
|
+
* Idempotent on an already-ready graph, because the honest answer to "publish
|
|
738
|
+
* this thing that is published" is the graph, not an error.
|
|
739
|
+
*/
|
|
740
|
+
publishWorkflow(id: string, publishedBy: string): Promise<CatalogWorkflow>;
|
|
741
|
+
/**
|
|
742
|
+
* Take a graph back to `draft`.
|
|
743
|
+
*
|
|
744
|
+
* **Refuses while any connector still runs it**, exactly as
|
|
745
|
+
* {@link deleteWorkflow} does and for the same reason: a connector may only
|
|
746
|
+
* point at a ready graph, so unpublishing one out from under a schedule breaks
|
|
747
|
+
* a load that was working, and the operator needs to know *which* connectors
|
|
748
|
+
* to point elsewhere first. Refusing here rather than cascading is deliberate —
|
|
749
|
+
* disabling somebody's connectors as a side effect of an edit to something
|
|
750
|
+
* else is precisely the silent action this status exists to prevent.
|
|
751
|
+
*/
|
|
752
|
+
unpublishWorkflow(id: string, unpublishedBy: string): Promise<CatalogWorkflow>;
|
|
673
753
|
/** Refuses while any connector still runs it. */
|
|
674
754
|
deleteWorkflow(id: string): Promise<boolean>;
|
|
675
755
|
/** Which connectors run it. Named, so a refusal can say. */
|
package/dist/catalog.pipeline.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* systems each believing they decide when a load runs.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
|
|
12
|
+
exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.CONNECTOR_KINDS = void 0;
|
|
13
13
|
exports.isConnectorKind = isConnectorKind;
|
|
14
14
|
exports.isTransformLanguage = isTransformLanguage;
|
|
15
15
|
exports.isWorkflowNodeKind = isWorkflowNodeKind;
|
|
16
|
+
exports.isWorkflowStatus = isWorkflowStatus;
|
|
16
17
|
exports.isWorkflowExecutionMode = isWorkflowExecutionMode;
|
|
17
18
|
exports.validateWorkflow = validateWorkflow;
|
|
18
19
|
exports.workflowRunOrder = workflowRunOrder;
|
|
@@ -136,6 +137,57 @@ function isWorkflowNodeKind(value) {
|
|
|
136
137
|
* separator would let one node read another's rows.
|
|
137
138
|
*/
|
|
138
139
|
exports.WORKFLOW_NODE_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
140
|
+
/**
|
|
141
|
+
* An authored graph of steps ending in one commit.
|
|
142
|
+
*
|
|
143
|
+
* Versioned the way {@link CatalogTransform} is, and for the same question: a
|
|
144
|
+
* load that produced surprising numbers is investigated afterwards, and "what
|
|
145
|
+
* ran" is the first thing asked. For a single-transform connector that means
|
|
146
|
+
* the code; for a workflow it means the code *and* the wiring, so both the
|
|
147
|
+
* graph version and the per-node transform versions are recorded on the run.
|
|
148
|
+
*
|
|
149
|
+
* Like a transform, only the latest shape is kept. Storing every past graph was
|
|
150
|
+
* the alternative and was rejected for consistency: transforms already answer
|
|
151
|
+
* "which code ran" with a number and no history, and a model where the graph is
|
|
152
|
+
* fully recoverable but the code inside it is not would give false confidence in
|
|
153
|
+
* an audit. The limitation is real and worth stating plainly — an edited graph
|
|
154
|
+
* cannot be reconstructed from an old run, only identified as different.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Whether this graph is still being drawn, or is something somebody declared
|
|
158
|
+
* finished.
|
|
159
|
+
*
|
|
160
|
+
* The distinction exists because validation used to be the gate on *saving*, and
|
|
161
|
+
* that made an unfinished graph unstorable: `saveWorkflow` refused anything
|
|
162
|
+
* `validateWorkflow` had an issue with, so a canvas with one node on it could
|
|
163
|
+
* not be written down at all and closing the tab lost it. Worse, it made the
|
|
164
|
+
* canvas lie about ordinary work — clicking "+ Sink" produces a node that is
|
|
165
|
+
* unreachable from any source and names no type, both true and both useless one
|
|
166
|
+
* second after the click, because a just-added node is unwired by construction.
|
|
167
|
+
*
|
|
168
|
+
* So the gate moved rather than loosened. Validation is now the gate on
|
|
169
|
+
* publishing, and the same `validateWorkflow` still decides — a draft is not a
|
|
170
|
+
* graph that skipped the rules, it is a graph nobody has claimed is finished
|
|
171
|
+
* yet. Everything that consumes a workflow asks for `ready`: a connector may
|
|
172
|
+
* only point at one, and a promotion may only carry one. What crosses an
|
|
173
|
+
* environment should be something a person declared done.
|
|
174
|
+
*/
|
|
175
|
+
exports.WORKFLOW_STATUSES = [
|
|
176
|
+
/**
|
|
177
|
+
* Being drawn. Saves without validating, and cannot run, be scheduled, or be
|
|
178
|
+
* promoted. An incomplete node here is the normal state rather than an alarm.
|
|
179
|
+
*/
|
|
180
|
+
'draft',
|
|
181
|
+
/**
|
|
182
|
+
* Declared finished, and validated at the moment it was declared. This is the
|
|
183
|
+
* only status a connector may point at and the only one a promotion carries.
|
|
184
|
+
*/
|
|
185
|
+
'ready',
|
|
186
|
+
];
|
|
187
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
188
|
+
function isWorkflowStatus(value) {
|
|
189
|
+
return exports.WORKFLOW_STATUSES.some((status) => status === value);
|
|
190
|
+
}
|
|
139
191
|
/**
|
|
140
192
|
* How a workflow run is executed here.
|
|
141
193
|
*
|
|
@@ -634,7 +686,13 @@ function isWorkflowEdge(value) {
|
|
|
634
686
|
function supportsWorkflows(store) {
|
|
635
687
|
return (typeof store.listWorkflows === 'function' &&
|
|
636
688
|
typeof store.getWorkflow === 'function' &&
|
|
637
|
-
typeof store.saveWorkflow === 'function'
|
|
689
|
+
typeof store.saveWorkflow === 'function' &&
|
|
690
|
+
// Asked for by name like the rest, rather than assumed to come with
|
|
691
|
+
// `saveWorkflow`. Promotion publishes what it saves, so a store that has the
|
|
692
|
+
// save and not the transition would narrow cleanly here and then fail one
|
|
693
|
+
// call later, in the middle of an apply that has already written types and
|
|
694
|
+
// transforms into the target.
|
|
695
|
+
typeof store.publishWorkflow === 'function');
|
|
638
696
|
}
|
|
639
697
|
function supportsWorkflowStages(store) {
|
|
640
698
|
return typeof store.writeStage === 'function' && typeof store.readStage === 'function';
|
|
@@ -59,9 +59,14 @@ export declare abstract class CatalogRegistry {
|
|
|
59
59
|
* request named) overriding is still right; deriving it a second time is not.
|
|
60
60
|
*/
|
|
61
61
|
getGraph(): CatalogGraph;
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Presentation-only edits. Never a schema change.
|
|
64
|
+
*
|
|
65
|
+
* @param curatedBy the acting principal's id, recorded on `type.curated`.
|
|
66
|
+
*/
|
|
67
|
+
abstract patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>, curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
|
|
68
|
+
/** @param curatedBy the acting principal's id, recorded on `type.curated`. */
|
|
69
|
+
abstract patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string], curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
|
|
65
70
|
/**
|
|
66
71
|
* Discard every tier-0 edit at once.
|
|
67
72
|
*
|
|
@@ -81,6 +86,12 @@ export declare abstract class CatalogRegistry {
|
|
|
81
86
|
* Which of those a deployment runs is why the event is worth more than the
|
|
82
87
|
* call it accompanies: a registry that quietly resets without emitting looks
|
|
83
88
|
* exactly like one that never ran a reset at all.
|
|
89
|
+
*
|
|
90
|
+
* @param resetBy the acting principal's id, recorded on `overlay.reset`. An
|
|
91
|
+
* implementation that refuses is free to declare no parameter at all — an
|
|
92
|
+
* override may take fewer than it was promised — and `StoredCatalogRegistry`
|
|
93
|
+
* does, because an argument it accepted and never recorded would read as a
|
|
94
|
+
* dropped actor rather than as a reset that never happened.
|
|
84
95
|
*/
|
|
85
|
-
abstract resetOverlay(): Promise<void>;
|
|
96
|
+
abstract resetOverlay(resetBy: string): Promise<void>;
|
|
86
97
|
}
|
|
@@ -32,9 +32,9 @@ export declare class MikroOrmCatalogRegistry extends CatalogRegistry implements
|
|
|
32
32
|
*/
|
|
33
33
|
getEntityClass(name: string): EntityClass<Record<string, unknown>> | undefined;
|
|
34
34
|
/** Tier-0 edit on a type. Never touches the database. */
|
|
35
|
-
patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]
|
|
35
|
+
patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>, curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
|
|
36
36
|
/** Tier-0 edit on a property. Never touches the database. */
|
|
37
|
-
patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string]): Promise<CatalogObjectTypeDef | undefined>;
|
|
37
|
+
patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string], curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
|
|
38
38
|
/**
|
|
39
39
|
* Drop every tier-0 edit, and leave a record that it happened.
|
|
40
40
|
*
|
|
@@ -45,8 +45,14 @@ export declare class MikroOrmCatalogRegistry extends CatalogRegistry implements
|
|
|
45
45
|
*
|
|
46
46
|
* Emitted after the write, like the two patches above, so the trail says what
|
|
47
47
|
* happened rather than what was about to.
|
|
48
|
+
*
|
|
49
|
+
* The actor is applied here rather than inside {@link summariseOverlay}, which
|
|
50
|
+
* stays a pure function of the overlay. What was destroyed and who destroyed it
|
|
51
|
+
* are facts from two different places, and folding the principal into the
|
|
52
|
+
* summariser would mean the one function that must be callable with nothing but
|
|
53
|
+
* an old overlay suddenly needing the request as well.
|
|
48
54
|
*/
|
|
49
|
-
resetOverlay(): Promise<void>;
|
|
55
|
+
resetOverlay(resetBy: string): Promise<void>;
|
|
50
56
|
private persist;
|
|
51
57
|
private rebuild;
|
|
52
58
|
private shouldInclude;
|