@dudousxd/nestjs-catalog 0.9.0 → 0.10.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.
@@ -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
- /** Drops every tier-0 edit and falls back to what the ORM says. */
167
- async reset() {
168
- await this.registry.resetOverlay();
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 change against.
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
@@ -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
- * **No `principalId`, and the absence is a limit rather than a decision that
127
- * the actor does not matter.** `resetOverlay()` takes no principal, the route
128
- * that calls it resolves none, and `RoutingCatalogRegistry` forwards the call
129
- * by hand — so a field here would be `undefined` on every row, and an audit
130
- * table lifts `principalId` into a column where empty reads as "nobody did
131
- * this" rather than "this was not captured". `type.curated` has the same gap.
132
- * Closing it means threading a principal through the controller, the service
133
- * and every registry, which is a change to those, not a field on this payload.
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. */
@@ -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
  *
@@ -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
- /** Presentation-only edits. Never a schema change. */
63
- abstract patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>): Promise<CatalogObjectTypeDef | undefined>;
64
- abstract patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string]): Promise<CatalogObjectTypeDef | undefined>;
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]>): Promise<CatalogObjectTypeDef | undefined>;
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;
@@ -179,7 +179,7 @@ let MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = class MikroOrmCatalogR
179
179
  return this.entityClasses.get(name);
180
180
  }
181
181
  /** Tier-0 edit on a type. Never touches the database. */
182
- async patchType(typeName, patch) {
182
+ async patchType(typeName, patch, curatedBy) {
183
183
  const type = this.getType(typeName);
184
184
  if (!type)
185
185
  return undefined;
@@ -192,11 +192,15 @@ let MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = class MikroOrmCatalogR
192
192
  (0, catalog_events_1.emitCatalog)('type.curated', {
193
193
  typeName: type.name,
194
194
  changed: Object.keys(rest),
195
+ // Through `curationActor` rather than passed straight in, even though the
196
+ // parameter is required: the callers this class actually has to survive are
197
+ // the ones the compiler never saw. See the note on that function.
198
+ principalId: (0, catalog_events_1.curationActor)(curatedBy),
195
199
  });
196
200
  return this.getType(type.name);
197
201
  }
198
202
  /** Tier-0 edit on a property. Never touches the database. */
199
- async patchProperty(typeName, propertyName, patch) {
203
+ async patchProperty(typeName, propertyName, patch, curatedBy) {
200
204
  const type = this.getType(typeName);
201
205
  if (!type)
202
206
  return undefined;
@@ -218,6 +222,7 @@ let MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = class MikroOrmCatalogR
218
222
  typeName: type.name,
219
223
  property: propertyName,
220
224
  changed: Object.keys(patch),
225
+ principalId: (0, catalog_events_1.curationActor)(curatedBy),
221
226
  });
222
227
  return this.getType(type.name);
223
228
  }
@@ -231,12 +236,18 @@ let MikroOrmCatalogRegistry = MikroOrmCatalogRegistry_1 = class MikroOrmCatalogR
231
236
  *
232
237
  * Emitted after the write, like the two patches above, so the trail says what
233
238
  * happened rather than what was about to.
239
+ *
240
+ * The actor is applied here rather than inside {@link summariseOverlay}, which
241
+ * stays a pure function of the overlay. What was destroyed and who destroyed it
242
+ * are facts from two different places, and folding the principal into the
243
+ * summariser would mean the one function that must be callable with nothing but
244
+ * an old overlay suddenly needing the request as well.
234
245
  */
235
- async resetOverlay() {
246
+ async resetOverlay(resetBy) {
236
247
  const discarded = summariseOverlay(this.overlay);
237
248
  this.overlay = { types: {} };
238
249
  await this.persist();
239
- (0, catalog_events_1.emitCatalog)('overlay.reset', discarded);
250
+ (0, catalog_events_1.emitCatalog)('overlay.reset', { ...discarded, principalId: (0, catalog_events_1.curationActor)(resetBy) });
240
251
  }
241
252
  async persist() {
242
253
  await this.overlayStore.save(this.overlay);
@@ -390,6 +401,13 @@ exports.MikroOrmCatalogRegistry = MikroOrmCatalogRegistry = MikroOrmCatalogRegis
390
401
  * order is forced: a caller has to hold the old overlay to call it, and cannot
391
402
  * accidentally summarise the empty one it just installed.
392
403
  *
404
+ * Everything the payload holds except the actor, stated as an `Omit` of the
405
+ * payload rather than a shape of its own. A hand-written interface here would be
406
+ * a second copy of the contract, free to fall behind the day a field is added —
407
+ * and the failure would be a summary silently missing a key that the type says
408
+ * is required. `principalId` is the caller's to supply because it is a fact about
409
+ * the request, not about the overlay.
410
+ *
393
411
  * A type entry counts whatever it holds, including an entry that ended up empty.
394
412
  * `buildType` treats a present entry as enrichment on the same terms, and the
395
413
  * honest reading of one is "somebody patched this type" — which is exactly what
@@ -27,11 +27,26 @@ export declare class CatalogService {
27
27
  getType(name: string): CatalogObjectTypeDef | undefined;
28
28
  /** Nodes and edges, for drawing the model. */
29
29
  getGraph(): CatalogGraph;
30
- /** Presentation-only. Never a schema change. */
31
- patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>): Promise<CatalogObjectTypeDef | undefined>;
32
- patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string]): Promise<CatalogObjectTypeDef | undefined>;
33
- /** Drops every runtime edit, where the registry supports it. */
34
- resetOverlay(): Promise<void>;
30
+ /**
31
+ * Presentation-only. Never a schema change.
32
+ *
33
+ * @param curatedBy who is doing it, for the audit trail the same required
34
+ * argument the sharing methods below take, and required for the same reason
35
+ * `deleteSavedQuery` gives. This facade forwards it rather than resolving it:
36
+ * a host writing its own controller has the request and this does not, and a
37
+ * default here would attribute every such host's curation to nobody.
38
+ */
39
+ patchType(typeName: string, patch: Partial<CatalogOverlay['types'][string]>, curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
40
+ /** @param curatedBy who is doing it, for the audit trail. */
41
+ patchProperty(typeName: string, propertyName: string, patch: NonNullable<CatalogOverlay['types'][string]['properties']>[string], curatedBy: string): Promise<CatalogObjectTypeDef | undefined>;
42
+ /**
43
+ * Drops every runtime edit, where the registry supports it.
44
+ *
45
+ * @param resetBy who is doing it. The one act here that destroys curation in
46
+ * bulk, so it is the one whose actor is hardest to reconstruct afterwards —
47
+ * nothing versions an overlay, and after this there is nothing left to read.
48
+ */
49
+ resetOverlay(resetBy: string): Promise<void>;
35
50
  /** Columns a generic UI may render: visible, and not a blob. */
36
51
  visibleColumns(type: CatalogObjectTypeDef): import("./catalog.types").CatalogPropertyDef[];
37
52
  /**
@@ -66,16 +66,31 @@ let CatalogService = class CatalogService {
66
66
  getGraph() {
67
67
  return this.registry.getGraph();
68
68
  }
69
- /** Presentation-only. Never a schema change. */
70
- patchType(typeName, patch) {
71
- return this.registry.patchType(typeName, patch);
69
+ /**
70
+ * Presentation-only. Never a schema change.
71
+ *
72
+ * @param curatedBy who is doing it, for the audit trail — the same required
73
+ * argument the sharing methods below take, and required for the same reason
74
+ * `deleteSavedQuery` gives. This facade forwards it rather than resolving it:
75
+ * a host writing its own controller has the request and this does not, and a
76
+ * default here would attribute every such host's curation to nobody.
77
+ */
78
+ patchType(typeName, patch, curatedBy) {
79
+ return this.registry.patchType(typeName, patch, curatedBy);
72
80
  }
73
- patchProperty(typeName, propertyName, patch) {
74
- return this.registry.patchProperty(typeName, propertyName, patch);
81
+ /** @param curatedBy who is doing it, for the audit trail. */
82
+ patchProperty(typeName, propertyName, patch, curatedBy) {
83
+ return this.registry.patchProperty(typeName, propertyName, patch, curatedBy);
75
84
  }
76
- /** Drops every runtime edit, where the registry supports it. */
77
- resetOverlay() {
78
- return this.registry.resetOverlay();
85
+ /**
86
+ * Drops every runtime edit, where the registry supports it.
87
+ *
88
+ * @param resetBy who is doing it. The one act here that destroys curation in
89
+ * bulk, so it is the one whose actor is hardest to reconstruct afterwards —
90
+ * nothing versions an overlay, and after this there is nothing left to read.
91
+ */
92
+ resetOverlay(resetBy) {
93
+ return this.registry.resetOverlay(resetBy);
79
94
  }
80
95
  /** Columns a generic UI may render: visible, and not a blob. */
81
96
  visibleColumns(type) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { CatalogProperty, type CatalogPropertyOptions, CatalogType, type CatalogTypeOptions, } from './catalog.decorators';
2
- export { CATALOG_EVENT_PHASE, CATALOG_EVENT_PHASE_FALLBACK, CATALOG_EVENTS, CATALOG_LIB, type CatalogEvent, type CatalogEventPayloads, catalogEventPhase, channelNameFor, emitCatalog, } from './catalog.events';
2
+ export { CATALOG_EVENT_PHASE, CATALOG_EVENT_PHASE_FALLBACK, CATALOG_EVENTS, CATALOG_LIB, type CatalogEvent, type CatalogEventPayloads, UNATTRIBUTED_PRINCIPAL_ID, catalogEventPhase, channelNameFor, curationActor, emitCatalog, } from './catalog.events';
3
3
  export { CatalogModule } from './catalog.module';
4
4
  export { assertReadOnlyShape, type CatalogQueryRelation, type CatalogQueryRequest, type CatalogQueryResult, type CatalogQueryStore, isQueryStore, } from './catalog.query';
5
5
  export { CATALOG_OPTIONS, type CatalogModuleOptions } from './catalog.options';
package/dist/index.js CHANGED
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.CATALOG_TRACE_OUTCOMES = exports.visibleToPrincipal = exports.searchCatalog = exports.maySearch = exports.emptySearch = exports.bestMatch = exports.MAX_SEARCH_LIMIT = exports.DEFAULT_SEARCH_LIMIT = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = exports.QueryCache = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.channelNameFor = exports.catalogEventPhase = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
18
- exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = void 0;
17
+ exports.searchCatalog = exports.maySearch = exports.emptySearch = exports.bestMatch = exports.MAX_SEARCH_LIMIT = exports.DEFAULT_SEARCH_LIMIT = exports.CatalogService = exports.SubprocessTransformRunner = exports.toCsv = exports.QueryCache = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.TRANSFORM_RUNNER = exports.supportsWorkflowStages = exports.supportsWorkflows = exports.isWorkflowNodeKind = exports.isWorkflowNode = exports.isWorkflowExecutionMode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isPipelineStore = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.CATALOG_PIPELINE_STORE = exports.CatalogRegistry = exports.MikroOrmCatalogRegistry = exports.CATALOG_OVERLAY_STORE = exports.InMemoryCatalogOverlayStore = exports.FileCatalogOverlayStore = exports.CATALOG_OPTIONS = exports.isQueryStore = exports.assertReadOnlyShape = exports.CatalogModule = exports.emitCatalog = exports.curationActor = exports.channelNameFor = exports.catalogEventPhase = exports.UNATTRIBUTED_PRINCIPAL_ID = exports.CATALOG_LIB = exports.CATALOG_EVENTS = exports.CATALOG_EVENT_PHASE_FALLBACK = exports.CATALOG_EVENT_PHASE = exports.CatalogType = exports.CatalogProperty = void 0;
18
+ exports.RequireScopes = exports.RequireHuman = exports.REQUIRES_HUMAN = exports.REQUIRED_SCOPES = exports.MikroOrmReadStore = exports.supportsCarryForward = exports.isWriteStore = exports.isReservedColumn = exports.isCatalogStoreCapabilities = exports.findColumnCollisions = exports.CatalogColumnCollisionError = exports.CATALOG_STORE = exports.CATALOG_SNAPSHOT_MODES = exports.CATALOG_RESERVED_COLUMNS = exports.assertNoColumnCollisions = exports.StaticKeyPrincipalResolver = exports.readableObjectPage = exports.mayWrite = exports.mayRead = exports.maySeeClassification = exports.PRINCIPAL_ACTOR_SEPARATOR = exports.parsePrincipalId = exports.hasScope = exports.expandScopes = exports.delegatePrincipal = exports.composePrincipalId = exports.CATALOG_PRINCIPAL_RESOLVER = exports.traceOutcomeFilter = exports.isWorkspaceStore = exports.isTraceStore = exports.isCatalogTraceOutcome = exports.embeddedVisualization = exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = exports.visibleToPrincipal = void 0;
19
19
  var catalog_decorators_1 = require("./catalog.decorators");
20
20
  Object.defineProperty(exports, "CatalogProperty", { enumerable: true, get: function () { return catalog_decorators_1.CatalogProperty; } });
21
21
  Object.defineProperty(exports, "CatalogType", { enumerable: true, get: function () { return catalog_decorators_1.CatalogType; } });
@@ -24,8 +24,10 @@ Object.defineProperty(exports, "CATALOG_EVENT_PHASE", { enumerable: true, get: f
24
24
  Object.defineProperty(exports, "CATALOG_EVENT_PHASE_FALLBACK", { enumerable: true, get: function () { return catalog_events_1.CATALOG_EVENT_PHASE_FALLBACK; } });
25
25
  Object.defineProperty(exports, "CATALOG_EVENTS", { enumerable: true, get: function () { return catalog_events_1.CATALOG_EVENTS; } });
26
26
  Object.defineProperty(exports, "CATALOG_LIB", { enumerable: true, get: function () { return catalog_events_1.CATALOG_LIB; } });
27
+ Object.defineProperty(exports, "UNATTRIBUTED_PRINCIPAL_ID", { enumerable: true, get: function () { return catalog_events_1.UNATTRIBUTED_PRINCIPAL_ID; } });
27
28
  Object.defineProperty(exports, "catalogEventPhase", { enumerable: true, get: function () { return catalog_events_1.catalogEventPhase; } });
28
29
  Object.defineProperty(exports, "channelNameFor", { enumerable: true, get: function () { return catalog_events_1.channelNameFor; } });
30
+ Object.defineProperty(exports, "curationActor", { enumerable: true, get: function () { return catalog_events_1.curationActor; } });
29
31
  Object.defineProperty(exports, "emitCatalog", { enumerable: true, get: function () { return catalog_events_1.emitCatalog; } });
30
32
  var catalog_module_1 = require("./catalog.module");
31
33
  Object.defineProperty(exports, "CatalogModule", { enumerable: true, get: function () { return catalog_module_1.CatalogModule; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-catalog",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
5
  "license": "MIT",
6
6
  "author": "Davide Carvalho",