@dudousxd/nestjs-catalog 0.11.0 → 0.13.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.
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CATALOG_STORE = exports.CatalogColumnCollisionError = exports.CATALOG_RESERVED_COLUMNS = exports.CATALOG_SNAPSHOT_MODES = void 0;
3
+ exports.CATALOG_STORE = exports.CatalogColumnCollisionError = exports.UnsafeIdentifierError = exports.CATALOG_RESERVED_COLUMNS = exports.CATALOG_SNAPSHOT_MODES = void 0;
4
4
  exports.isCatalogStoreCapabilities = isCatalogStoreCapabilities;
5
5
  exports.isReservedColumn = isReservedColumn;
6
+ exports.isSafeIdentifier = isSafeIdentifier;
7
+ exports.assertSafeIdentifier = assertSafeIdentifier;
6
8
  exports.findColumnCollisions = findColumnCollisions;
7
9
  exports.assertNoColumnCollisions = assertNoColumnCollisions;
8
10
  exports.isWriteStore = isWriteStore;
@@ -72,6 +74,73 @@ exports.CATALOG_RESERVED_COLUMNS = [
72
74
  function isReservedColumn(column) {
73
75
  return exports.CATALOG_RESERVED_COLUMNS.some((reserved) => reserved === column.toLowerCase());
74
76
  }
77
+ /**
78
+ * What a name has to look like before a store will write it into SQL.
79
+ *
80
+ * Identifiers are *rejected*, never escaped and never sanitised. Every table
81
+ * and column name a store emits arrives from another application over HTTP and
82
+ * ends up in DDL and in SELECT lists, where no placeholder can stand in for it,
83
+ * so anything outside this character set never becomes SQL at all.
84
+ *
85
+ * Here, beside {@link CATALOG_RESERVED_COLUMNS}, for the same reason: it is
86
+ * part of what the catalog promises a *publisher*. Refuse a property name and
87
+ * the sentence explaining why is the only statement of the rule most people
88
+ * will ever read, so it belongs to the contract rather than to whichever
89
+ * adapter happens to be mounted.
90
+ *
91
+ * And for one more reason. It used to be two copies — `store-mikro-orm` and
92
+ * `store-clickhouse` each carried this pattern and this sentence, byte for
93
+ * byte — and the publish-time refusal in the pipeline package borrowed the
94
+ * MySQL one so that publish-time and DDL-time could not disagree about the
95
+ * character set, the length or the wording. That bought the guarantee for a
96
+ * MySQL deployment and left a ClickHouse-only one trusting two files to be
97
+ * edited together. One definition is the guarantee; two identical ones are a
98
+ * habit.
99
+ *
100
+ * 63 characters because it is under MySQL's 64-character ceiling and no engine
101
+ * a store here targets refuses a name that short, and because the number is
102
+ * quoted in the refusal below: a per-store limit would mean a publisher being
103
+ * told a different rule depending on what is mounted, for a name the catalog
104
+ * would then be unable to promise anything about across a fan-out.
105
+ *
106
+ * Not exported. A `RegExp` is mutable and shared state, and the two questions
107
+ * anyone has of it — "may I?" and "why not?" — are {@link isSafeIdentifier} and
108
+ * {@link UnsafeIdentifierError}.
109
+ */
110
+ const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
111
+ /**
112
+ * Why a name cannot be written into SQL, in the words a publisher is given.
113
+ *
114
+ * One class for the whole ecosystem rather than one per adapter, so
115
+ * `instanceof` is a usable question across packages. The publish-time check in
116
+ * the pipeline package catches this to tell "that name cannot be an identifier"
117
+ * from "something else failed inside the store", and with a class per adapter
118
+ * that check would re-throw the moment the mounted store was not the one it
119
+ * imported — turning a 400 that names the property into a 500 that names
120
+ * nothing.
121
+ */
122
+ class UnsafeIdentifierError extends Error {
123
+ constructor(value) {
124
+ super(`Refusing to use "${value}" as a SQL identifier: letters, digits and underscore only, starting with a letter or underscore, 63 characters max.`);
125
+ }
126
+ }
127
+ exports.UnsafeIdentifierError = UnsafeIdentifierError;
128
+ /** Whether a name can be written into SQL as it stands. */
129
+ function isSafeIdentifier(value) {
130
+ return SAFE_IDENTIFIER.test(value);
131
+ }
132
+ /**
133
+ * Refuse a name that cannot be a SQL identifier.
134
+ *
135
+ * Throws rather than answering, because the caller's next line writes the value
136
+ * into a statement: a boolean that can be ignored is a boolean that eventually
137
+ * is. {@link isSafeIdentifier} is there for the callers that are asking rather
138
+ * than about to build.
139
+ */
140
+ function assertSafeIdentifier(value) {
141
+ if (!isSafeIdentifier(value))
142
+ throw new UnsafeIdentifierError(value);
143
+ }
75
144
  /**
76
145
  * Every way a type's properties would fight over a physical column.
77
146
  *
@@ -11,6 +11,16 @@ export interface SavedQuery {
11
11
  id: string;
12
12
  name: string;
13
13
  description?: string;
14
+ /**
15
+ * The statement, as it is now.
16
+ *
17
+ * Overwritten in place by {@link CatalogWorkspaceStore.updateSavedQuery}, and
18
+ * for a long time that was the end of it — a report that started answering
19
+ * differently left nothing to compare against, not even a version number. What
20
+ * it used to say is kept as {@link CatalogRevision}s now, read through
21
+ * {@link CatalogWorkspaceStore.listSavedQueryRevisions}; this field stays the
22
+ * one a run of the query executes.
23
+ */
14
24
  sql: string;
15
25
  /** Free-form grouping, the way a folder would work without being one. */
16
26
  folder?: string;
@@ -71,6 +81,122 @@ export interface SaveQueryInput {
71
81
  visualization?: QueryVisualization;
72
82
  shared?: boolean;
73
83
  }
84
+ /**
85
+ * One recorded revision of something whose text a person edits.
86
+ *
87
+ * The same shape for a transform's code and for a saved query's SQL, and that is
88
+ * the point rather than a saving: the two are edited the same way and go wrong
89
+ * the same way — somebody changes the text, a load or a report starts coming out
90
+ * different, and the question afterwards is what the text used to say. `body` is
91
+ * named for that. `code` would have been a lie on half of its uses.
92
+ *
93
+ * ## What this exists to fix
94
+ *
95
+ * A {@link ConnectorRun} has always recorded `transformVersion`, so the catalog
96
+ * already knew *which* version produced a given load. What it did not keep was
97
+ * the text of that version: one row per transform, overwritten in place, the
98
+ * counter bumped and the previous code gone. A saved query had not even the
99
+ * counter. Meanwhile the runs list renders `code v3`, which reads as a reference
100
+ * to something retrievable — so an operator was told a version number, believed
101
+ * the source was recoverable, and it was not.
102
+ *
103
+ * The number on the run and the {@link version} here are **the same number**.
104
+ * That is the whole contract: "this load ran v3" becomes something a person can
105
+ * open.
106
+ *
107
+ * ## No `kind` field, deliberately
108
+ *
109
+ * A revision is always read through a route that already names its subject —
110
+ * `transforms/:id/revisions`, `saved-queries/:id/revisions` — so a discriminator
111
+ * here would be a field whose only possible value the caller had just supplied.
112
+ * The *store* keys by one, because one table holds both kinds; that is a storage
113
+ * concern and it stays in the store.
114
+ *
115
+ * ## What is NOT revisioned, and why
116
+ *
117
+ * A workflow graph, which has the identical "latest only" limitation and is
118
+ * deliberately left with it. See {@link CatalogWorkflow}, which makes the
119
+ * argument where somebody looking for the missing feature will find it.
120
+ */
121
+ export interface CatalogRevision {
122
+ /**
123
+ * Stable id of this revision.
124
+ *
125
+ * Derived from the subject and the version rather than random — see
126
+ * `revisionKey` in the MikroORM store — so recording the same version twice
127
+ * replaces it instead of appending a second copy, and a screen may key a list
128
+ * on it across refetches.
129
+ */
130
+ id: string;
131
+ /** What it belongs to — a transform id or a saved-query id. */
132
+ subjectId: string;
133
+ /** The version this revision IS. Matches `transformVersion` on a run. */
134
+ version: number;
135
+ /** The text as it was: the transform's code, or the query's SQL. */
136
+ body: string;
137
+ /**
138
+ * Who saved it.
139
+ *
140
+ * Exact for a transform, which is saved through a store method that is given
141
+ * the actor. **Approximate for a saved query**, whose update path is given
142
+ * none — `updateSavedQuery` takes an id and a patch, and `CatalogService`
143
+ * keeps the actor for the audit event it emits — so a saved query's revisions
144
+ * are attributed to the query's `createdBy`. That is who created it, not
145
+ * necessarily who last edited it, and it is recorded that way rather than
146
+ * invented: a name here that was picked to fill the field would be read as
147
+ * evidence. Threading the editor through `updateSavedQuery` is what would fix
148
+ * it, and it is a change to that method's contract rather than to this one.
149
+ */
150
+ authoredBy: string;
151
+ authoredAt: string;
152
+ }
153
+ /**
154
+ * How many revisions are kept per subject. Writing a newer one drops the oldest
155
+ * beyond this.
156
+ *
157
+ * ## Why there is a cap at all
158
+ *
159
+ * This is append-only text that grows forever, and it is the fourth append-only
160
+ * table in the bundled store. The other three earn their unboundedness and this
161
+ * one does not. An audit event and a connector run are each one small row per
162
+ * *thing that happened*, at a rate an operator can read off their own load
163
+ * schedule; staged rows are dropped the moment the run that produced them is
164
+ * finished with. A revision is neither: it grows with how often somebody edits,
165
+ * which nobody meters, and every row carries a whole code body rather than a
166
+ * counter. Unpredictable in rate *and* large per row is the combination worth
167
+ * bounding — and "this grows; here is the query to prune it" would have been a
168
+ * fourth unbounded table with a paragraph in front of it.
169
+ *
170
+ * ## Why a count per subject rather than an age bound
171
+ *
172
+ * An age bound keys the wrong thing. A transform edited twice in 2019 and relied
173
+ * on ever since would lose both revisions, while one edited daily keeps
174
+ * everything — exactly backwards. What makes a revision unrecoverable is being
175
+ * superseded, so that is what the bound counts.
176
+ *
177
+ * ## What it costs, stated rather than implied
178
+ *
179
+ * A run's `transformVersion` can name a revision that has been evicted: a
180
+ * transform saved more than this many times can no longer produce its earliest
181
+ * code. That loss is real. It is strictly smaller than the one it replaces —
182
+ * where every version but the newest was unrecoverable — and it is visible
183
+ * rather than silent, because a caller holding a version older than the oldest
184
+ * revision in the list can see that the list does not reach that far.
185
+ *
186
+ * At the cap a 4 KB body costs 200 KB per subject, so a thousand heavily-edited
187
+ * subjects cost roughly 200 MB. That is a ceiling, which is the point of having
188
+ * one.
189
+ *
190
+ * ## Why a constant and not a module option
191
+ *
192
+ * The number is part of what this table promises, and a console should be able
193
+ * to print "the last 50 are kept" without a round trip to ask which deployment
194
+ * it is talking to. A knob is also a promise to support every value of it,
195
+ * including the one that switches the feature off and is then reported as a bug.
196
+ * It becomes an option on the day there is a deployment it is wrong for, rather
197
+ * than in anticipation of one.
198
+ */
199
+ export declare const CATALOG_REVISION_LIMIT = 50;
74
200
  export interface Dashboard {
75
201
  id: string;
76
202
  name: string;
@@ -480,6 +606,26 @@ export interface CatalogWorkspaceStore {
480
606
  * first and decides.
481
607
  */
482
608
  deleteSavedQuery(id: string): Promise<boolean>;
609
+ /**
610
+ * Every SQL this saved query has ever been, newest first.
611
+ *
612
+ * **Optional**, and mixed in here rather than made a member every store must
613
+ * have, for the reason {@link CatalogWorkflowStore} gives about the same
614
+ * decision: a store written against the previous shape of this interface —
615
+ * including the routing proxy in the MikroORM package — implements
616
+ * `CatalogWorkspaceStore` today, and turning that into a compile error would
617
+ * be a breaking change for a feature that is purely additive.
618
+ * {@link supportsSavedQueryRevisions} is how a caller asks, so "this store
619
+ * keeps no revisions" is a sentence a route can say rather than a method that
620
+ * is missing at run time.
621
+ *
622
+ * An EMPTY list is a real answer and never an error: a query nobody has edited
623
+ * since this shipped may genuinely have nothing recorded. What the bundled
624
+ * store does about that — see `readRevisions` — is a store's decision, and a
625
+ * consumer must read "nothing recorded" as itself rather than as "nothing has
626
+ * changed".
627
+ */
628
+ listSavedQueryRevisions?(id: string): Promise<CatalogRevision[]>;
483
629
  listDashboards(): Promise<Dashboard[]>;
484
630
  getDashboard(id: string): Promise<Dashboard | undefined>;
485
631
  saveDashboard(input: {
@@ -498,5 +644,13 @@ export interface CatalogWorkspaceStore {
498
644
  recordEvent(event: Omit<CatalogAuditEvent, 'id'>): Promise<void>;
499
645
  listEvents(query: AuditQuery): Promise<CatalogAuditEvent[]>;
500
646
  }
647
+ /**
648
+ * Whether this store keeps a saved query's history.
649
+ *
650
+ * Checks the method rather than a flag, the same way {@link isWorkspaceStore}
651
+ * and `supportsWorkflows` do: a flag is a claim and a method is the thing
652
+ * itself.
653
+ */
654
+ export declare function supportsSavedQueryRevisions(store: CatalogWorkspaceStore): store is CatalogWorkspaceStore & Required<Pick<CatalogWorkspaceStore, 'listSavedQueryRevisions'>>;
501
655
  export declare function isWorkspaceStore(store: unknown): store is CatalogWorkspaceStore;
502
656
  export declare const CATALOG_WORKSPACE_STORE: unique symbol;
@@ -9,12 +9,60 @@
9
9
  * mount the catalog without any of this and notice nothing missing.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = void 0;
12
+ exports.CATALOG_WORKSPACE_STORE = exports.CATALOG_TRACE_STORE = exports.CATALOG_TRACE_OUTCOMES = exports.CATALOG_REVISION_LIMIT = void 0;
13
13
  exports.isCatalogTraceOutcome = isCatalogTraceOutcome;
14
14
  exports.traceOutcomeFilter = traceOutcomeFilter;
15
15
  exports.isTraceStore = isTraceStore;
16
16
  exports.embeddedVisualization = embeddedVisualization;
17
+ exports.supportsSavedQueryRevisions = supportsSavedQueryRevisions;
17
18
  exports.isWorkspaceStore = isWorkspaceStore;
19
+ /**
20
+ * How many revisions are kept per subject. Writing a newer one drops the oldest
21
+ * beyond this.
22
+ *
23
+ * ## Why there is a cap at all
24
+ *
25
+ * This is append-only text that grows forever, and it is the fourth append-only
26
+ * table in the bundled store. The other three earn their unboundedness and this
27
+ * one does not. An audit event and a connector run are each one small row per
28
+ * *thing that happened*, at a rate an operator can read off their own load
29
+ * schedule; staged rows are dropped the moment the run that produced them is
30
+ * finished with. A revision is neither: it grows with how often somebody edits,
31
+ * which nobody meters, and every row carries a whole code body rather than a
32
+ * counter. Unpredictable in rate *and* large per row is the combination worth
33
+ * bounding — and "this grows; here is the query to prune it" would have been a
34
+ * fourth unbounded table with a paragraph in front of it.
35
+ *
36
+ * ## Why a count per subject rather than an age bound
37
+ *
38
+ * An age bound keys the wrong thing. A transform edited twice in 2019 and relied
39
+ * on ever since would lose both revisions, while one edited daily keeps
40
+ * everything — exactly backwards. What makes a revision unrecoverable is being
41
+ * superseded, so that is what the bound counts.
42
+ *
43
+ * ## What it costs, stated rather than implied
44
+ *
45
+ * A run's `transformVersion` can name a revision that has been evicted: a
46
+ * transform saved more than this many times can no longer produce its earliest
47
+ * code. That loss is real. It is strictly smaller than the one it replaces —
48
+ * where every version but the newest was unrecoverable — and it is visible
49
+ * rather than silent, because a caller holding a version older than the oldest
50
+ * revision in the list can see that the list does not reach that far.
51
+ *
52
+ * At the cap a 4 KB body costs 200 KB per subject, so a thousand heavily-edited
53
+ * subjects cost roughly 200 MB. That is a ceiling, which is the point of having
54
+ * one.
55
+ *
56
+ * ## Why a constant and not a module option
57
+ *
58
+ * The number is part of what this table promises, and a console should be able
59
+ * to print "the last 50 are kept" without a round trip to ask which deployment
60
+ * it is talking to. A knob is also a promise to support every value of it,
61
+ * including the one that switches the feature off and is then reported as a bug.
62
+ * It becomes an option on the day there is a deployment it is wrong for, rather
63
+ * than in anticipation of one.
64
+ */
65
+ exports.CATALOG_REVISION_LIMIT = 50;
18
66
  /**
19
67
  * The same events, told as stories instead of as a list.
20
68
  *
@@ -102,6 +150,16 @@ function embeddedVisualization(saved, cardLibrary) {
102
150
  const base = saved ?? { kind: 'table' };
103
151
  return cardLibrary ? { ...base, library: cardLibrary } : base;
104
152
  }
153
+ /**
154
+ * Whether this store keeps a saved query's history.
155
+ *
156
+ * Checks the method rather than a flag, the same way {@link isWorkspaceStore}
157
+ * and `supportsWorkflows` do: a flag is a claim and a method is the thing
158
+ * itself.
159
+ */
160
+ function supportsSavedQueryRevisions(store) {
161
+ return typeof store.listSavedQueryRevisions === 'function';
162
+ }
105
163
  function isWorkspaceStore(store) {
106
164
  return (typeof store === 'object' &&
107
165
  store !== null &&
package/dist/client.d.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  * own UI. The endpoints alone are not an API; the endpoints plus the response
10
10
  * types are.
11
11
  */
12
- export type { AuditQuery, CatalogAuditEvent, Dashboard, DashboardCard, QueryVisualization, SaveQueryInput, SavedQuery, } from './catalog.workspace';
12
+ import type { DeleteReconciliation, LoadExpectation, RowCountBound, StoredLoadExpectation } from './catalog.pipeline';
13
+ export type { AuditQuery, CatalogAuditEvent, CatalogRevision, Dashboard, DashboardCard, QueryVisualization, SaveQueryInput, SavedQuery, } from './catalog.workspace';
14
+ export { CATALOG_REVISION_LIMIT } from './catalog.workspace';
13
15
  export type { CatalogQueryRelation, CatalogQueryRequest, CatalogQueryResult, } from './catalog.query';
14
16
  export type { CatalogSearchField, CatalogSearchHit, CatalogSearchKind, CatalogSearchRank, CatalogSearchResult, } from './search.types';
15
17
  export type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogPropertyDef, CatalogRelationDef, CatalogSnapshot, RelationKind, ScalarType, } from './catalog.types';
@@ -66,6 +68,15 @@ export declare const catalogRoutes: {
66
68
  readonly workspaceCapabilities: () => string;
67
69
  readonly savedQueries: () => string;
68
70
  readonly savedQuery: (id: string) => string;
71
+ /**
72
+ * Every SQL this query has ever been.
73
+ *
74
+ * A sub-resource of the saved query rather than a `?version=` on it, because
75
+ * the question a diff screen asks first is "what were all of them" — it has to
76
+ * see the list before it knows which two to compare, and one request that
77
+ * answers that beats a list plus two fetches.
78
+ */
79
+ readonly savedQueryRevisions: (id: string) => string;
69
80
  readonly runSavedQuery: (id: string) => string;
70
81
  readonly exportSavedQuery: (id: string) => string;
71
82
  readonly dashboards: () => string;
@@ -96,3 +107,111 @@ export { CONNECTOR_KINDS, isConnectorKind, isTransformLanguage, isWorkflowEdge,
96
107
  export { validateWorkflow, WORKFLOW_EXECUTION_MODES, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_STATUSES, workflowGraphHash, workflowRunOrder, isWorkflowExecutionMode, isWorkflowNodeKind, isWorkflowStatus, } from './catalog.pipeline';
97
108
  export type { WorkflowStatus } from './catalog.pipeline';
98
109
  export type { CatalogTrace, CatalogTraceList, CatalogTraceOutcome, CatalogTraceSpan, TraceQuery, } from './catalog.workspace';
110
+ export type { CatalogLoadExpectations, DeleteReconciliation, LoadExpectation, RowCountBound, StoredLoadExpectation, } from './catalog.pipeline';
111
+ /**
112
+ * The three answers to "how do deletions at the source reach this type", as a
113
+ * value.
114
+ *
115
+ * A list rather than only the union, because the editor on the Model screen has
116
+ * to offer them and a hand-written array in a component is the copy that drifts
117
+ * — the same argument {@link CONNECTOR_KINDS} makes one screen over. The server
118
+ * validates against this too: a `strategy` off the wire is `string` until
119
+ * something checks it, and the check and the dropdown reading one list is what
120
+ * stops the two from disagreeing about what is acceptable.
121
+ *
122
+ * **There are three and there is deliberately no fourth.** Tombstones off a
123
+ * change feed is the correct answer and needs machinery nothing here has; a
124
+ * strategy name that nothing implements is a dropdown with a lie in it. See
125
+ * `DeleteReconciliation` for the whole argument.
126
+ */
127
+ export declare const DELETE_RECONCILIATION_STRATEGIES: readonly ["accepted", "soft-deleted-at-source", "periodic-full-reload"];
128
+ export type DeleteReconciliationStrategy = (typeof DELETE_RECONCILIATION_STRATEGIES)[number];
129
+ /** Whether a value off the wire names one of the three. */
130
+ export declare function isDeleteReconciliationStrategy(value: unknown): value is DeleteReconciliationStrategy;
131
+ /**
132
+ * What an operator sends to set a type's expectation.
133
+ *
134
+ * Both fields optional and both meaning "leave this alone" when absent, which is
135
+ * why they are not simply `LoadExpectation`: a request that omits `rowCount`
136
+ * has said nothing about row counts, and reading that as "clear it" would let a
137
+ * form that only renders the delete strategy silently drop a bound somebody set.
138
+ * Clearing the whole stored row is `DELETE`, which is a decision with a verb on
139
+ * it.
140
+ */
141
+ export interface LoadExpectationInput {
142
+ deletes?: DeleteReconciliation;
143
+ rowCount?: Partial<RowCountBound>;
144
+ }
145
+ /**
146
+ * The resolved expectation for one type, and which layer won each field.
147
+ *
148
+ * The provenance is the whole reason this is not just a `LoadExpectation`. The
149
+ * policy is sourced from three layers — `host.byType[type]`, then the stored row
150
+ * an operator set, then `host.default` — and a screen that showed only the
151
+ * answer would let somebody edit a field this deployment has pinned in code and
152
+ * watch the edit vanish on the next read, with nothing anywhere saying why.
153
+ * {@link hostLocked} is what lets it say "this deployment fixed it" instead.
154
+ */
155
+ export interface ResolvedLoadExpectation {
156
+ typeName: string;
157
+ /** The three layers merged, field by field. What the load is actually judged against. */
158
+ resolved: LoadExpectation;
159
+ /**
160
+ * Which layer supplied the delete strategy.
161
+ *
162
+ * `'default'` means the host's house-wide `default` did. `'none'` means
163
+ * nothing did, anywhere — which is not a gap to be filled in silently: it is
164
+ * the state that refuses every incremental load of this type, and the one the
165
+ * screen most needs to name.
166
+ */
167
+ deletesFrom: 'host' | 'stored' | 'default' | 'none';
168
+ /**
169
+ * Which layer supplied the row-count bound — the strongest one that set any
170
+ * field of it, since the three are merged key by key rather than replaced
171
+ * whole.
172
+ *
173
+ * There is no `'none'`, and that is a statement rather than an omission: a
174
+ * bound always applies. Where no layer says anything the built-in
175
+ * `DEFAULT_ROW_COUNT_BOUND` does, and that is what `'default'` covers as well
176
+ * as the host's own `default`.
177
+ */
178
+ rowCountFrom: 'host' | 'stored' | 'default';
179
+ /** The operator's row, present whether or not it won anything. */
180
+ stored?: StoredLoadExpectation;
181
+ /**
182
+ * Which fields this deployment declared in code, per field.
183
+ *
184
+ * True means a stored value for that field would never apply, so the editor
185
+ * shows it disabled and says so. Only `host.byType[type]` locks: `host.default`
186
+ * is the weakest layer and an operator's row beats it, so a house-wide default
187
+ * is not a lock and must not be drawn as one.
188
+ */
189
+ hostLocked: {
190
+ deletes: boolean;
191
+ rowCount: boolean;
192
+ };
193
+ }
194
+ /**
195
+ * Where the per-type expectations sit, relative to wherever the pipeline
196
+ * controller was mounted.
197
+ *
198
+ * A function of the base path rather than a frozen object like
199
+ * {@link catalogRoutes}, and the difference is the one `routes.ts` in the React
200
+ * package draws: the catalog controller's paths cannot move, and these move with
201
+ * whatever `path` the host passed to `CatalogPipelineModule.forRoot` — the
202
+ * routes below are `<path>/pipeline/expectations`, and `/pipeline` is only the
203
+ * default.
204
+ *
205
+ * Two builders for four routes: `PUT` and `DELETE` address the same path a `GET`
206
+ * of one type does, and a builder per verb would be three names for one string.
207
+ */
208
+ export declare function pipelineExpectationRoutes(basePath?: string): {
209
+ /** Every stored row, plus the types the host has locked. `GET`, `catalog:read`. */
210
+ readonly expectations: () => string;
211
+ /**
212
+ * One type: `GET` for the resolved expectation and its provenance, `PUT` to
213
+ * set the stored row, `DELETE` to drop it. The writes need `catalog:curate`
214
+ * and a signed-in person.
215
+ */
216
+ readonly expectation: (typeName: string) => string;
217
+ };
package/dist/client.js CHANGED
@@ -11,7 +11,14 @@
11
11
  * types are.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = void 0;
14
+ exports.DELETE_RECONCILIATION_STRATEGIES = exports.isWorkflowStatus = exports.isWorkflowNodeKind = exports.isWorkflowExecutionMode = exports.workflowRunOrder = exports.workflowGraphHash = exports.WORKFLOW_STATUSES = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_EXECUTION_MODES = exports.validateWorkflow = exports.TRANSFORM_LANGUAGES = exports.isWorkflowNode = exports.isWorkflowEdge = exports.isTransformLanguage = exports.isConnectorKind = exports.CONNECTOR_KINDS = exports.catalogRoutes = exports.CATALOG_REVISION_LIMIT = void 0;
15
+ exports.isDeleteReconciliationStrategy = isDeleteReconciliationStrategy;
16
+ exports.pipelineExpectationRoutes = pipelineExpectationRoutes;
17
+ // A value, not a type: a screen saying how far back the history goes should read
18
+ // the number rather than print one of its own. See its docblock for what the cap
19
+ // costs.
20
+ var catalog_workspace_1 = require("./catalog.workspace");
21
+ Object.defineProperty(exports, "CATALOG_REVISION_LIMIT", { enumerable: true, get: function () { return catalog_workspace_1.CATALOG_REVISION_LIMIT; } });
15
22
  /**
16
23
  * Builds the paths the catalog controller serves, relative to wherever it was
17
24
  * mounted. Kept as string builders rather than a fetch wrapper so the host
@@ -40,6 +47,15 @@ exports.catalogRoutes = {
40
47
  workspaceCapabilities: () => '/catalog/workspace/capabilities',
41
48
  savedQueries: () => '/catalog/saved-queries',
42
49
  savedQuery: (id) => `/catalog/saved-queries/${encodeURIComponent(id)}`,
50
+ /**
51
+ * Every SQL this query has ever been.
52
+ *
53
+ * A sub-resource of the saved query rather than a `?version=` on it, because
54
+ * the question a diff screen asks first is "what were all of them" — it has to
55
+ * see the list before it knows which two to compare, and one request that
56
+ * answers that beats a list plus two fetches.
57
+ */
58
+ savedQueryRevisions: (id) => `/catalog/saved-queries/${encodeURIComponent(id)}/revisions`,
43
59
  runSavedQuery: (id) => `/catalog/saved-queries/${encodeURIComponent(id)}/run`,
44
60
  exportSavedQuery: (id) => `/catalog/saved-queries/${encodeURIComponent(id)}/export.csv`,
45
61
  dashboards: () => '/catalog/dashboards',
@@ -99,3 +115,56 @@ Object.defineProperty(exports, "workflowRunOrder", { enumerable: true, get: func
99
115
  Object.defineProperty(exports, "isWorkflowExecutionMode", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowExecutionMode; } });
100
116
  Object.defineProperty(exports, "isWorkflowNodeKind", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowNodeKind; } });
101
117
  Object.defineProperty(exports, "isWorkflowStatus", { enumerable: true, get: function () { return catalog_pipeline_2.isWorkflowStatus; } });
118
+ /**
119
+ * The three answers to "how do deletions at the source reach this type", as a
120
+ * value.
121
+ *
122
+ * A list rather than only the union, because the editor on the Model screen has
123
+ * to offer them and a hand-written array in a component is the copy that drifts
124
+ * — the same argument {@link CONNECTOR_KINDS} makes one screen over. The server
125
+ * validates against this too: a `strategy` off the wire is `string` until
126
+ * something checks it, and the check and the dropdown reading one list is what
127
+ * stops the two from disagreeing about what is acceptable.
128
+ *
129
+ * **There are three and there is deliberately no fourth.** Tombstones off a
130
+ * change feed is the correct answer and needs machinery nothing here has; a
131
+ * strategy name that nothing implements is a dropdown with a lie in it. See
132
+ * `DeleteReconciliation` for the whole argument.
133
+ */
134
+ exports.DELETE_RECONCILIATION_STRATEGIES = [
135
+ 'accepted',
136
+ 'soft-deleted-at-source',
137
+ 'periodic-full-reload',
138
+ ];
139
+ /** Whether a value off the wire names one of the three. */
140
+ function isDeleteReconciliationStrategy(value) {
141
+ return (typeof value === 'string' &&
142
+ exports.DELETE_RECONCILIATION_STRATEGIES.includes(value));
143
+ }
144
+ /**
145
+ * Where the per-type expectations sit, relative to wherever the pipeline
146
+ * controller was mounted.
147
+ *
148
+ * A function of the base path rather than a frozen object like
149
+ * {@link catalogRoutes}, and the difference is the one `routes.ts` in the React
150
+ * package draws: the catalog controller's paths cannot move, and these move with
151
+ * whatever `path` the host passed to `CatalogPipelineModule.forRoot` — the
152
+ * routes below are `<path>/pipeline/expectations`, and `/pipeline` is only the
153
+ * default.
154
+ *
155
+ * Two builders for four routes: `PUT` and `DELETE` address the same path a `GET`
156
+ * of one type does, and a builder per verb would be three names for one string.
157
+ */
158
+ function pipelineExpectationRoutes(basePath = '/pipeline') {
159
+ const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
160
+ return {
161
+ /** Every stored row, plus the types the host has locked. `GET`, `catalog:read`. */
162
+ expectations: () => `${base}/expectations`,
163
+ /**
164
+ * One type: `GET` for the resolved expectation and its provenance, `PUT` to
165
+ * set the stored row, `DELETE` to drop it. The writes need `catalog:curate`
166
+ * and a signed-in person.
167
+ */
168
+ expectation: (typeName) => `${base}/expectations/${encodeURIComponent(typeName)}`,
169
+ };
170
+ }
package/dist/index.d.ts CHANGED
@@ -8,17 +8,17 @@ export { type CatalogOverlayStore, FileCatalogOverlayStore, InMemoryCatalogOverl
8
8
  export { CATALOG_OVERLAY_STORE } from './catalog.overlay-store.token';
9
9
  export { MikroOrmCatalogRegistry } from './catalog.registry';
10
10
  export { CatalogRegistry } from './catalog.registry.base';
11
- export { CATALOG_PIPELINE_STORE, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type ConnectorKind, type ConnectorRun, isConnectorKind, isPipelineStore, isTransformLanguage, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowNode, isWorkflowNodeKind, isWorkflowStatus, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, validateWorkflow, WORKFLOW_EXECUTION_MODES, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_STATUSES, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowGraph, workflowGraphHash, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, type WorkflowNodeOutcome, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowSinkNode, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowStatus, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
11
+ export { CATALOG_PIPELINE_STORE, type CatalogConnection, type CatalogConnector, type ConnectionCheck, CONNECTOR_KINDS, type CatalogLoadExpectations, type CatalogLoadExpectationStore, type CatalogPipelineStore, type CatalogStageStore, type CatalogTransform, type CatalogWorkflow, type CatalogWorkflowCapabilities, type CatalogWorkflowStore, type ConnectorKind, type ConnectorRun, type DeleteReconciliation, isConnectorKind, isPipelineStore, isTransformLanguage, type LoadExpectation, type RowCountBound, type StoredLoadExpectation, supportsLoadExpectations, supportsTransformRevisions, isWorkflowEdge, isWorkflowExecutionMode, isWorkflowNode, isWorkflowNodeKind, isWorkflowStatus, supportsWorkflows, supportsWorkflowStages, TRANSFORM_RUNNER, TRANSFORM_LANGUAGES, type TransformLanguage, type TransformResult, type TransformRunner, validateWorkflow, WORKFLOW_EXECUTION_MODES, WORKFLOW_ISSUE_CODES, WORKFLOW_NODE_ID_PATTERN, WORKFLOW_NODE_KINDS, WORKFLOW_STATUSES, type WorkflowEdge, type WorkflowExecutionMode, type WorkflowGraph, workflowGraphHash, type WorkflowIssueCode, type WorkflowNode, type WorkflowNodeKind, type WorkflowNodeOutcome, type WorkflowNodeStepInput, type WorkflowNodeStepOutput, workflowRunOrder, type WorkflowSinkNode, type WorkflowSourceNode, type WorkflowStageRef, type WorkflowStatus, type WorkflowTransformNode, type WorkflowValidationIssue, } from './catalog.pipeline';
12
12
  export * from './catalog.environment';
13
13
  export { QueryCache, toCsv } from './catalog.query-cache';
14
14
  export { SubprocessTransformRunner, type TransformRunnerOptions, } from './transform-runner';
15
15
  export { CatalogService } from './catalog.service';
16
16
  export { DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, bestMatch, emptySearch, maySearch, type SearchInput, type SearchableDashboard, type SearchableSavedQuery, searchCatalog, visibleToPrincipal, } from './search';
17
17
  export type { CatalogSearchField, CatalogSearchHit, CatalogSearchKind, CatalogSearchRank, CatalogSearchResult, } from './search.types';
18
- export { type AuditQuery, CATALOG_TRACE_OUTCOMES, CATALOG_TRACE_STORE, CATALOG_WORKSPACE_STORE, type CatalogAuditEvent, type CatalogTrace, type CatalogTraceList, type CatalogTraceOutcome, type CatalogTraceSpan, type CatalogTraceStore, type CatalogTraceTotals, type CatalogUnlinkedList, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, type EmbeddedChart, type EmbeddedChartPlacement, type EmbeddedDashboard, embeddedVisualization, isCatalogTraceOutcome, isTraceStore, isWorkspaceStore, type QueryVisualization, type SaveQueryInput, type SavedQuery, type TraceQuery, traceOutcomeFilter, } from './catalog.workspace';
18
+ export { type AuditQuery, CATALOG_REVISION_LIMIT, CATALOG_TRACE_OUTCOMES, CATALOG_TRACE_STORE, CATALOG_WORKSPACE_STORE, type CatalogAuditEvent, type CatalogRevision, type CatalogTrace, type CatalogTraceList, type CatalogTraceOutcome, type CatalogTraceSpan, type CatalogTraceStore, type CatalogTraceTotals, type CatalogUnlinkedList, type CatalogWorkspaceStore, type Dashboard, type DashboardCard, type EmbeddedChart, type EmbeddedChartPlacement, type EmbeddedDashboard, embeddedVisualization, isCatalogTraceOutcome, isTraceStore, isWorkspaceStore, type QueryVisualization, type SaveQueryInput, type SavedQuery, supportsSavedQueryRevisions, type TraceQuery, traceOutcomeFilter, } from './catalog.workspace';
19
19
  export { CATALOG_PRINCIPAL_RESOLVER, type CatalogActor, type CatalogGrants, type CatalogPrincipal, type CatalogPrincipalResolver, type CatalogScope, composePrincipalId, delegatePrincipal, expandScopes, hasScope, parsePrincipalId, PRINCIPAL_ACTOR_SEPARATOR, maySeeClassification, mayRead, mayWrite, readableObjectPage, StaticKeyPrincipalResolver, } from './catalog.principal';
20
20
  export * from './catalog.access';
21
- export { assertNoColumnCollisions, CATALOG_RESERVED_COLUMNS, CATALOG_SNAPSHOT_MODES, CATALOG_STORE, type CarryForwardResult, type CatalogColumnCollision, CatalogColumnCollisionError, type CatalogMergeStore, type CatalogReadQuery, type CatalogReadResult, type CatalogReadStore, type CatalogReservedColumn, type CatalogSnapshotMode, type CatalogStoreCapabilities, type CatalogWriteStore, type ColumnCollisionOptions, findColumnCollisions, isCatalogStoreCapabilities, isReservedColumn, isWriteStore, type SnapshotRef, supportsCarryForward, } from './catalog.store';
21
+ export { assertNoColumnCollisions, assertSafeIdentifier, CATALOG_RESERVED_COLUMNS, CATALOG_SNAPSHOT_MODES, CATALOG_STORE, type CarryForwardResult, type CatalogColumnCollision, CatalogColumnCollisionError, type CatalogMergeStore, type CatalogReadQuery, type CatalogReadResult, type CatalogReadStore, type CatalogReservedColumn, type CatalogSnapshotMode, type CatalogStoreCapabilities, type CatalogWriteStore, type ColumnCollisionOptions, findColumnCollisions, isCatalogStoreCapabilities, isReservedColumn, isSafeIdentifier, isWriteStore, type SnapshotRef, supportsCarryForward, UnsafeIdentifierError, } from './catalog.store';
22
22
  export { MikroOrmReadStore } from './stores/mikro-orm-read.store';
23
23
  export type { CatalogGraph, CatalogObjectPage, CatalogObjectQuery, CatalogObjectTypeDef, CatalogOverlay, CatalogPropertyDef, CatalogRelationDef, CatalogSnapshot, RelationKind, ScalarType, } from './catalog.types';
24
24
  export { REQUIRED_SCOPES, REQUIRES_HUMAN, RequireHuman, RequireScopes, } from './catalog.route-auth';