@dudousxd/nestjs-catalog 0.1.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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/catalog.controller.d.ts +8 -0
- package/dist/catalog.controller.js +482 -0
- package/dist/catalog.decorators.d.ts +37 -0
- package/dist/catalog.decorators.js +50 -0
- package/dist/catalog.environment.d.ts +442 -0
- package/dist/catalog.environment.js +645 -0
- package/dist/catalog.events.d.ts +179 -0
- package/dist/catalog.events.js +110 -0
- package/dist/catalog.module.d.ts +5 -0
- package/dist/catalog.module.js +71 -0
- package/dist/catalog.options.d.ts +79 -0
- package/dist/catalog.options.js +4 -0
- package/dist/catalog.overlay-store.d.ts +25 -0
- package/dist/catalog.overlay-store.js +44 -0
- package/dist/catalog.overlay-store.token.d.ts +1 -0
- package/dist/catalog.overlay-store.token.js +4 -0
- package/dist/catalog.pipeline.d.ts +800 -0
- package/dist/catalog.pipeline.js +606 -0
- package/dist/catalog.principal.d.ts +209 -0
- package/dist/catalog.principal.js +245 -0
- package/dist/catalog.query-cache.d.ts +25 -0
- package/dist/catalog.query-cache.js +0 -0
- package/dist/catalog.query.d.ts +76 -0
- package/dist/catalog.query.js +64 -0
- package/dist/catalog.registry.base.d.ts +21 -0
- package/dist/catalog.registry.base.js +17 -0
- package/dist/catalog.registry.d.ts +44 -0
- package/dist/catalog.registry.js +359 -0
- package/dist/catalog.service.d.ts +115 -0
- package/dist/catalog.service.js +366 -0
- package/dist/catalog.store.d.ts +419 -0
- package/dist/catalog.store.js +175 -0
- package/dist/catalog.types.d.ts +165 -0
- package/dist/catalog.types.js +19 -0
- package/dist/catalog.workspace.d.ts +426 -0
- package/dist/catalog.workspace.js +87 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +109 -0
- package/dist/stores/mikro-orm-read.store.d.ts +20 -0
- package/dist/stores/mikro-orm-read.store.js +120 -0
- package/dist/transform-runner.d.ts +54 -0
- package/dist/transform-runner.js +280 -0
- package/package.json +54 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting data in: where it comes from, and what turns it into rows.
|
|
3
|
+
*
|
|
4
|
+
* The shape NiFi and Airflow both settle on — a source, a transform, a sink —
|
|
5
|
+
* with the sink fixed, because the sink is the whole point of a catalog. What
|
|
6
|
+
* is deliberately *not* here is a scheduler: the durable engine already
|
|
7
|
+
* schedules, retries and checkpoints, and writing a second one would mean two
|
|
8
|
+
* systems each believing they decide when a load runs.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Where a connector pulls from.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately a short list. Every kind here is one this service can actually
|
|
14
|
+
* execute — a kind that exists in the type and throws at run time is worse than
|
|
15
|
+
* one that is absent, because the first looks supported in a dropdown.
|
|
16
|
+
*/
|
|
17
|
+
export declare const CONNECTOR_KINDS: readonly ["http", "sql", "file", "s3", "inline"];
|
|
18
|
+
export type ConnectorKind = (typeof CONNECTOR_KINDS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* The type is derived from the list, not written beside it.
|
|
21
|
+
*
|
|
22
|
+
* A second hand-maintained copy of these names is the bug this shape exists to
|
|
23
|
+
* prevent: a store that narrows a database string against its own stale array
|
|
24
|
+
* and falls back to a default turns "the kind you chose" into "the kind that
|
|
25
|
+
* happened to be first", and the resulting failure names the wrong source
|
|
26
|
+
* entirely. Anything narrowing a stored value narrows against *this*.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isConnectorKind(value: unknown): value is ConnectorKind;
|
|
29
|
+
export interface CatalogConnector {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
kind: ConnectorKind;
|
|
34
|
+
/** Which object type its records become. */
|
|
35
|
+
targetType: string;
|
|
36
|
+
/**
|
|
37
|
+
* Source configuration. Never credentials — those are referenced by the name
|
|
38
|
+
* of an environment variable, so the catalog stores the *name* of a secret
|
|
39
|
+
* and never the secret.
|
|
40
|
+
*/
|
|
41
|
+
config: Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* The named connection this reads through, if it uses one.
|
|
44
|
+
*
|
|
45
|
+
* When set, the connection supplies the address and the credential and the
|
|
46
|
+
* connector's own `config` carries only what is specific to this load — the
|
|
47
|
+
* query, the prefix, the path. Inline configuration stays supported because a
|
|
48
|
+
* one-off source does not deserve a second object to manage.
|
|
49
|
+
*/
|
|
50
|
+
connectionId?: string;
|
|
51
|
+
/** Env var holding the credential, if the source needs one. */
|
|
52
|
+
secretEnvVar?: string;
|
|
53
|
+
/** The transform that turns source records into rows of `targetType`. */
|
|
54
|
+
transformId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The workflow that turns source records into rows of `targetType`, when one
|
|
57
|
+
* transform is not enough.
|
|
58
|
+
*
|
|
59
|
+
* Mutually exclusive with {@link transformId}, and the store refuses a
|
|
60
|
+
* connector that sets both: two answers to "what shapes this data" means the
|
|
61
|
+
* runner picks one, and which one it picked is invisible until the load comes
|
|
62
|
+
* out wrong. A connector with a `transformId` and no `workflowId` behaves
|
|
63
|
+
* exactly as it did before workflows existed.
|
|
64
|
+
*
|
|
65
|
+
* When this is set, the connector's own `kind`, `config`, `connectionId` and
|
|
66
|
+
* `secretEnvVar` are **not read**: the workflow's source nodes say where the
|
|
67
|
+
* data comes from, and letting the connector also say would be two authorities
|
|
68
|
+
* for one question. `targetType` stays meaningful and is kept equal to the
|
|
69
|
+
* workflow's sink type by {@link CatalogPipelineStore.saveConnector}, so every
|
|
70
|
+
* existing "which connectors write this type" answer keeps working.
|
|
71
|
+
*
|
|
72
|
+
* `state` keeps its meaning too, but is keyed by node id when a workflow runs:
|
|
73
|
+
* a graph with two sources has two watermarks, and one flat blob would let
|
|
74
|
+
* them overwrite each other.
|
|
75
|
+
*/
|
|
76
|
+
workflowId?: string;
|
|
77
|
+
/** Cron-ish, interpreted by whatever schedules it. Empty means manual only. */
|
|
78
|
+
schedule?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Whether a run replaces the dataset or adds to it.
|
|
81
|
+
*
|
|
82
|
+
* `full` is the default and the one the snapshot model is shaped for: a run
|
|
83
|
+
* reads everything, writes a complete snapshot, and the commit repoints the
|
|
84
|
+
* view atomically. `incremental` reads only what changed since the last run
|
|
85
|
+
* and carries the rest forward, which is cheaper but needs the source to
|
|
86
|
+
* offer a watermark and the type to have a primary key to merge on.
|
|
87
|
+
*/
|
|
88
|
+
mode?: 'full' | 'incremental';
|
|
89
|
+
/**
|
|
90
|
+
* Where the last run got to. Written by the runner, never by a person.
|
|
91
|
+
*
|
|
92
|
+
* Separate from `config` on purpose: config is authored and reviewed, state
|
|
93
|
+
* is a consequence. Mixing them means a person editing a connector can
|
|
94
|
+
* silently rewind or skip data, and a diff of the config stops meaning what
|
|
95
|
+
* somebody decided.
|
|
96
|
+
*/
|
|
97
|
+
state?: Record<string, unknown>;
|
|
98
|
+
enabled: boolean;
|
|
99
|
+
createdBy: string;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
lastRunAt?: string;
|
|
103
|
+
lastRunStatus?: 'succeeded' | 'failed' | 'running';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* TypeScript is Node's own type stripping, so it costs no compiler and no build
|
|
107
|
+
* step — and types are erased, never checked. A transform with a wrong type
|
|
108
|
+
* still runs; the editor's try pane is what catches it.
|
|
109
|
+
*/
|
|
110
|
+
export declare const TRANSFORM_LANGUAGES: readonly ["javascript", "typescript", "python"];
|
|
111
|
+
export type TransformLanguage = (typeof TRANSFORM_LANGUAGES)[number];
|
|
112
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
113
|
+
export declare function isTransformLanguage(value: unknown): value is TransformLanguage;
|
|
114
|
+
/**
|
|
115
|
+
* User code that maps a source record to a row.
|
|
116
|
+
*
|
|
117
|
+
* Versioned, because a load that produced surprising numbers is investigated
|
|
118
|
+
* afterwards, and "which code ran" is the first question. Bumping the version
|
|
119
|
+
* on every change costs a row and answers it.
|
|
120
|
+
*/
|
|
121
|
+
export interface CatalogTransform {
|
|
122
|
+
id: string;
|
|
123
|
+
name: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
language: TransformLanguage;
|
|
126
|
+
/**
|
|
127
|
+
* The body of a function over one batch. It receives `records` and returns
|
|
128
|
+
* the rows to store.
|
|
129
|
+
*
|
|
130
|
+
* A batch rather than a record at a time: a transform that needs to look up,
|
|
131
|
+
* deduplicate or aggregate cannot do it one row at a time, and paying one
|
|
132
|
+
* process spawn per record would make any real load unusable.
|
|
133
|
+
*/
|
|
134
|
+
code: string;
|
|
135
|
+
version: number;
|
|
136
|
+
createdBy: string;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
}
|
|
140
|
+
export interface TransformResult {
|
|
141
|
+
rows: Array<Record<string, unknown>>;
|
|
142
|
+
/** Anything the code logged. Surfaced in the run, never in the rows. */
|
|
143
|
+
logs: string[];
|
|
144
|
+
elapsedMs: number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Runs user code.
|
|
148
|
+
*
|
|
149
|
+
* An interface because the isolation a deployment needs is a deployment
|
|
150
|
+
* decision. The bundled runner spawns a child process with a timeout and no
|
|
151
|
+
* inherited environment, which stops an accident — an infinite loop, a stray
|
|
152
|
+
* `process.env.DATABASE_PASSWORD` — but it is **not a security boundary**
|
|
153
|
+
* against code written to escape one. A catalog that accepts transforms from
|
|
154
|
+
* people who are not already trusted with the database needs a container or a
|
|
155
|
+
* sandboxed runtime, and this interface is where that gets plugged in.
|
|
156
|
+
*/
|
|
157
|
+
export interface TransformRunner {
|
|
158
|
+
run(transform: Pick<CatalogTransform, 'language' | 'code'>, records: unknown[], options?: {
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
}): Promise<TransformResult>;
|
|
161
|
+
/** Languages this runner can actually execute in this environment. */
|
|
162
|
+
available(): Promise<TransformLanguage[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Python libraries importable here, if the runner can tell.
|
|
165
|
+
*
|
|
166
|
+
* Reported rather than assumed: "pandas is available" is a property of the
|
|
167
|
+
* image, and a UI that promises it on an image without it turns a deployment
|
|
168
|
+
* difference into a traceback the transform's author cannot act on.
|
|
169
|
+
*/
|
|
170
|
+
pythonPackages?(): Promise<string[]>;
|
|
171
|
+
}
|
|
172
|
+
export declare const TRANSFORM_RUNNER: unique symbol;
|
|
173
|
+
export interface ConnectorRun {
|
|
174
|
+
id: string;
|
|
175
|
+
connectorId: string;
|
|
176
|
+
/** The snapshot this run wrote, which is also the durable run id. */
|
|
177
|
+
snapshotId: string;
|
|
178
|
+
principalId: string;
|
|
179
|
+
status: 'running' | 'succeeded' | 'failed';
|
|
180
|
+
fetched: number;
|
|
181
|
+
written: number;
|
|
182
|
+
logs: string[];
|
|
183
|
+
error?: string;
|
|
184
|
+
startedAt: string;
|
|
185
|
+
finishedAt?: string;
|
|
186
|
+
/** Which transform version ran, so a surprising load can be traced to code. */
|
|
187
|
+
transformVersion?: number;
|
|
188
|
+
/** Which workflow ran, when the connector delegated to one. */
|
|
189
|
+
workflowId?: string;
|
|
190
|
+
/**
|
|
191
|
+
* Which *version* of it ran.
|
|
192
|
+
*
|
|
193
|
+
* The same question `transformVersion` answers, asked of the graph. A
|
|
194
|
+
* workflow keeps only its latest shape — exactly as a transform keeps only
|
|
195
|
+
* its latest code — so this number is what connects a run to the graph that
|
|
196
|
+
* produced it, and the only way to know a graph has changed since.
|
|
197
|
+
*/
|
|
198
|
+
workflowVersion?: number;
|
|
199
|
+
/**
|
|
200
|
+
* The fingerprint of the graph at that version.
|
|
201
|
+
*
|
|
202
|
+
* Recorded beside the version rather than instead of it, because a version
|
|
203
|
+
* number is only unique within one catalog database. A workflow promoted from
|
|
204
|
+
* dev to production carries its own numbering, so two runs in two
|
|
205
|
+
* environments can both say "version 4" and mean different graphs; the hash
|
|
206
|
+
* cannot. Cheap enough that not storing it would be the only reason to face
|
|
207
|
+
* that question later with nothing to answer it.
|
|
208
|
+
*/
|
|
209
|
+
graphHash?: string;
|
|
210
|
+
/**
|
|
211
|
+
* How this run actually executed — checkpointed per node, or not.
|
|
212
|
+
*
|
|
213
|
+
* A fact about what happened, not a setting. A deployment with
|
|
214
|
+
* `CATALOG_DURABLE=off` still runs workflows; it simply restarts them from
|
|
215
|
+
* the first node when they fail, and a run list that did not say so would let
|
|
216
|
+
* an operator believe a ten-node graph resumed at node seven when it did not.
|
|
217
|
+
*/
|
|
218
|
+
executionMode?: WorkflowExecutionMode;
|
|
219
|
+
/**
|
|
220
|
+
* What each node did, keyed by node id.
|
|
221
|
+
*
|
|
222
|
+
* One JSON column rather than a `failedNodeId` scalar plus a version map,
|
|
223
|
+
* because both questions asked of a failed run — "where did it stop" and
|
|
224
|
+
* "which code ran up to there" — are answered by the same per-node record,
|
|
225
|
+
* and two half-answers can disagree. Bounded by the node count, never by the
|
|
226
|
+
* row count: no rows are ever put in here.
|
|
227
|
+
*/
|
|
228
|
+
nodeOutcomes?: Record<string, WorkflowNodeOutcome>;
|
|
229
|
+
}
|
|
230
|
+
/** What one node did during a run. Small by construction — counters, not rows. */
|
|
231
|
+
export interface WorkflowNodeOutcome {
|
|
232
|
+
/**
|
|
233
|
+
* `skipped` exists for the nodes downstream of a failure. Without it, a
|
|
234
|
+
* ten-node graph that died at node seven records three nodes with no entry at
|
|
235
|
+
* all, which reads the same as three nodes nobody has looked at yet.
|
|
236
|
+
*/
|
|
237
|
+
status: 'succeeded' | 'failed' | 'skipped';
|
|
238
|
+
/** Rows this node produced, or committed if it is the sink. */
|
|
239
|
+
rows: number;
|
|
240
|
+
/** For a transform node: which version of its code ran. */
|
|
241
|
+
transformVersion?: number;
|
|
242
|
+
elapsedMs?: number;
|
|
243
|
+
error?: string;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* What a node can be.
|
|
247
|
+
*
|
|
248
|
+
* Three kinds, and they are exactly the three verbs the existing connector
|
|
249
|
+
* runner already performs in sequence: fetch, transform, publish. Nothing here
|
|
250
|
+
* is a kind this service cannot execute, which is the same rule
|
|
251
|
+
* {@link CONNECTOR_KINDS} follows — a kind that exists in the type and throws
|
|
252
|
+
* at run time is worse than one that is absent, because the first looks
|
|
253
|
+
* supported in a palette.
|
|
254
|
+
*
|
|
255
|
+
* The kinds that were considered and rejected, since a small vocabulary is only
|
|
256
|
+
* defensible if the omissions are:
|
|
257
|
+
*
|
|
258
|
+
* - **filter** — a transform whose code returns a subset of what it was given.
|
|
259
|
+
* It needs no new execution path, only a different body, and adding the kind
|
|
260
|
+
* would mean two ways to drop rows and two places to look when rows go
|
|
261
|
+
* missing.
|
|
262
|
+
* - **branch / split** — already expressible: a node with two outbound edges is
|
|
263
|
+
* read by both successors, each of which filters differently. There is
|
|
264
|
+
* nothing for a branch node to *do*.
|
|
265
|
+
* - **merge / join** — a node with several inbound edges receives its inputs
|
|
266
|
+
* concatenated in edge order (see {@link WorkflowEdge}). A keyed join is then
|
|
267
|
+
* ordinary code inside the transform, which can already see every record.
|
|
268
|
+
* A `merge` kind would have had to carry a strategy field whose values the
|
|
269
|
+
* runner would have to implement one by one, and an unimplemented strategy in
|
|
270
|
+
* a dropdown is the failure this list exists to avoid.
|
|
271
|
+
*/
|
|
272
|
+
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink"];
|
|
273
|
+
export type WorkflowNodeKind = (typeof WORKFLOW_NODE_KINDS)[number];
|
|
274
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
275
|
+
export declare function isWorkflowNodeKind(value: unknown): value is WorkflowNodeKind;
|
|
276
|
+
/**
|
|
277
|
+
* The longest a node id may be, and the alphabet it may use.
|
|
278
|
+
*
|
|
279
|
+
* Constrained rather than free-form for two concrete reasons. A node id becomes
|
|
280
|
+
* the name of a durable step, and durable step names are how a replay finds the
|
|
281
|
+
* checkpoint it already wrote — a step renamed between runs re-executes work
|
|
282
|
+
* that was already done. And staged rows are addressed by a key built from the
|
|
283
|
+
* run id, the node id and the batch number, so a node id containing the
|
|
284
|
+
* separator would let one node read another's rows.
|
|
285
|
+
*/
|
|
286
|
+
export declare const WORKFLOW_NODE_ID_PATTERN: RegExp;
|
|
287
|
+
interface WorkflowNodeBase {
|
|
288
|
+
/** Unique within the workflow. Also the durable step name. */
|
|
289
|
+
id: string;
|
|
290
|
+
/** What a person calls it. Cosmetic: changing it does not bump the version. */
|
|
291
|
+
name: string;
|
|
292
|
+
/**
|
|
293
|
+
* Where the canvas drew it.
|
|
294
|
+
*
|
|
295
|
+
* Persisted even though it changes nothing about execution, because a layout
|
|
296
|
+
* a person arranged and the server then forgot is a canvas that loses work.
|
|
297
|
+
* Excluded from the graph fingerprint for the same reason a rename is: moving
|
|
298
|
+
* a box is not a new version of the graph.
|
|
299
|
+
*/
|
|
300
|
+
position?: {
|
|
301
|
+
x: number;
|
|
302
|
+
y: number;
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Reads records out of a system.
|
|
307
|
+
*
|
|
308
|
+
* Carries the same vocabulary a connector does — a kind, an optional named
|
|
309
|
+
* connection, a config, the *name* of an env var holding the credential — and
|
|
310
|
+
* for the same reason {@link CatalogConnection} does: a source and a connector
|
|
311
|
+
* reaching the same system must agree about what they are talking to, and two
|
|
312
|
+
* vocabularies would let them disagree. Credentials stay out of the catalog
|
|
313
|
+
* here exactly as they do everywhere else.
|
|
314
|
+
*/
|
|
315
|
+
export interface WorkflowSourceNode extends WorkflowNodeBase {
|
|
316
|
+
kind: 'source';
|
|
317
|
+
/** Named `sourceKind` rather than `kind`, which the union already uses. */
|
|
318
|
+
sourceKind: ConnectorKind;
|
|
319
|
+
connectionId?: string;
|
|
320
|
+
config: Record<string, unknown>;
|
|
321
|
+
secretEnvVar?: string;
|
|
322
|
+
/**
|
|
323
|
+
* Whether this source reads everything or only what changed. Per node,
|
|
324
|
+
* because a graph can perfectly well enrich a full pull against an
|
|
325
|
+
* incrementally-read lookup table.
|
|
326
|
+
*/
|
|
327
|
+
mode?: 'full' | 'incremental';
|
|
328
|
+
}
|
|
329
|
+
/** Runs user code over the rows its inbound edges carry. */
|
|
330
|
+
export interface WorkflowTransformNode extends WorkflowNodeBase {
|
|
331
|
+
kind: 'transform';
|
|
332
|
+
/**
|
|
333
|
+
* The transform to run. A reference rather than inline code, so one piece of
|
|
334
|
+
* logic used at three points in a graph is versioned once and fixed once.
|
|
335
|
+
*/
|
|
336
|
+
transformId: string;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Writes into an object type and commits.
|
|
340
|
+
*
|
|
341
|
+
* There is exactly one of these per workflow, and that is the load-bearing
|
|
342
|
+
* decision of this whole model rather than a simplification. A workflow writing
|
|
343
|
+
* both `Mvr` and `Subwo` makes "commit" ambiguous — both atomically, or one
|
|
344
|
+
* succeeding while the other fails — which is the distributed-transaction
|
|
345
|
+
* problem, and answering it is not something a catalog should take on to buy a
|
|
346
|
+
* convenience that two workflows already provide. Branching inside the graph
|
|
347
|
+
* stays fully supported; every path simply has to arrive here.
|
|
348
|
+
*/
|
|
349
|
+
export interface WorkflowSinkNode extends WorkflowNodeBase {
|
|
350
|
+
kind: 'sink';
|
|
351
|
+
/** Which object type the rows become. */
|
|
352
|
+
targetType: string;
|
|
353
|
+
/**
|
|
354
|
+
* Whether the commit replaces the dataset or merges into it. Exactly the
|
|
355
|
+
* meaning {@link CatalogConnector.mode} has, at the node that actually does
|
|
356
|
+
* the committing.
|
|
357
|
+
*/
|
|
358
|
+
mode?: 'full' | 'incremental';
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* A discriminated union, so narrowing a node is `node.kind === "sink"` and
|
|
362
|
+
* never a type assertion. This is why the kind list is not simply a string on
|
|
363
|
+
* one node shape with every field optional: that shape lets a source node carry
|
|
364
|
+
* a `transformId` and nothing catches it.
|
|
365
|
+
*/
|
|
366
|
+
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode;
|
|
367
|
+
/**
|
|
368
|
+
* One wire.
|
|
369
|
+
*
|
|
370
|
+
* No id of its own: duplicate edges are refused, so `from` and `to` together
|
|
371
|
+
* already identify a wire, and an id would be a second identity that a canvas
|
|
372
|
+
* could let drift from the pair that actually matters.
|
|
373
|
+
*
|
|
374
|
+
* **Order is meaningful.** A node with several inbound edges receives its
|
|
375
|
+
* inputs in the order those edges appear in {@link CatalogWorkflow.edges}, and
|
|
376
|
+
* that order is what the transform sees. It is preserved rather than sorted, and
|
|
377
|
+
* it is part of the graph fingerprint: swapping two inputs to a join changes
|
|
378
|
+
* what the load produces, so it is a new version of the graph.
|
|
379
|
+
*/
|
|
380
|
+
export interface WorkflowEdge {
|
|
381
|
+
from: string;
|
|
382
|
+
to: string;
|
|
383
|
+
}
|
|
384
|
+
/** Just the executable part of a workflow, for validating a canvas draft. */
|
|
385
|
+
export interface WorkflowGraph {
|
|
386
|
+
nodes: WorkflowNode[];
|
|
387
|
+
edges: WorkflowEdge[];
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* An authored graph of steps ending in one commit.
|
|
391
|
+
*
|
|
392
|
+
* Versioned the way {@link CatalogTransform} is, and for the same question: a
|
|
393
|
+
* load that produced surprising numbers is investigated afterwards, and "what
|
|
394
|
+
* ran" is the first thing asked. For a single-transform connector that means
|
|
395
|
+
* the code; for a workflow it means the code *and* the wiring, so both the
|
|
396
|
+
* graph version and the per-node transform versions are recorded on the run.
|
|
397
|
+
*
|
|
398
|
+
* Like a transform, only the latest shape is kept. Storing every past graph was
|
|
399
|
+
* the alternative and was rejected for consistency: transforms already answer
|
|
400
|
+
* "which code ran" with a number and no history, and a model where the graph is
|
|
401
|
+
* fully recoverable but the code inside it is not would give false confidence in
|
|
402
|
+
* an audit. The limitation is real and worth stating plainly — an edited graph
|
|
403
|
+
* cannot be reconstructed from an old run, only identified as different.
|
|
404
|
+
*/
|
|
405
|
+
export interface CatalogWorkflow {
|
|
406
|
+
id: string;
|
|
407
|
+
name: string;
|
|
408
|
+
description?: string;
|
|
409
|
+
nodes: WorkflowNode[];
|
|
410
|
+
edges: WorkflowEdge[];
|
|
411
|
+
/** Bumped whenever the graph's behaviour changes. Never on a rename or a move. */
|
|
412
|
+
version: number;
|
|
413
|
+
/** Fingerprint of the graph at this version. See {@link workflowGraphHash}. */
|
|
414
|
+
graphHash: string;
|
|
415
|
+
/**
|
|
416
|
+
* The type the sink writes.
|
|
417
|
+
*
|
|
418
|
+
* Derived from the sink node and stored beside it anyway, which is normally a
|
|
419
|
+
* smell. It is safe here precisely because validation guarantees exactly one
|
|
420
|
+
* sink, so the two cannot disagree, and it buys the two things a JSON column
|
|
421
|
+
* cannot: "which workflows write this type" as a query, and a cheap check when
|
|
422
|
+
* a connector claims to write something else.
|
|
423
|
+
*/
|
|
424
|
+
targetType: string;
|
|
425
|
+
createdBy: string;
|
|
426
|
+
createdAt: string;
|
|
427
|
+
updatedAt: string;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* How a workflow run is executed here.
|
|
431
|
+
*
|
|
432
|
+
* `CATALOG_DURABLE` is `own`, `attach` or `off`, so a deployment may genuinely
|
|
433
|
+
* have no engine, and the model must not pretend otherwise. A workflow that
|
|
434
|
+
* appeared checkpointed and was not would be the same silent no-op this codebase
|
|
435
|
+
* already had to remove once, when connectors carried a `schedule` field that
|
|
436
|
+
* nothing read for months.
|
|
437
|
+
*/
|
|
438
|
+
export declare const WORKFLOW_EXECUTION_MODES: readonly ["durable", "inline"];
|
|
439
|
+
export type WorkflowExecutionMode = (typeof WORKFLOW_EXECUTION_MODES)[number];
|
|
440
|
+
export declare function isWorkflowExecutionMode(value: unknown): value is WorkflowExecutionMode;
|
|
441
|
+
/**
|
|
442
|
+
* What this deployment can actually do with a workflow, in words a UI can print.
|
|
443
|
+
*
|
|
444
|
+
* Reported by the host rather than stored on the workflow, because it is a
|
|
445
|
+
* property of the pods that are running, not of the graph: the same workflow is
|
|
446
|
+
* checkpointed on a worker with `CATALOG_DURABLE=own` and not on an API pod with
|
|
447
|
+
* it off. A console that says "resumes where it failed" on a deployment that
|
|
448
|
+
* cannot is worse than one that says nothing.
|
|
449
|
+
*/
|
|
450
|
+
export interface CatalogWorkflowCapabilities {
|
|
451
|
+
mode: WorkflowExecutionMode;
|
|
452
|
+
/** Why. Something like `CATALOG_DURABLE=off on this pod, so runs restart.` */
|
|
453
|
+
detail: string;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* A handle to rows a node produced. **This is the only thing that crosses a
|
|
457
|
+
* step boundary.**
|
|
458
|
+
*
|
|
459
|
+
* Not a style preference — a measurement. `durable_step_checkpoints` persists
|
|
460
|
+
* each step's input and output as JSON, so a node contract that passed rows
|
|
461
|
+
* would write the entire intermediate dataset into the durable store once per
|
|
462
|
+
* node, ten times over for a ten-node graph, and a replay would then read it all
|
|
463
|
+
* back. The existing connector step already gets this right by returning
|
|
464
|
+
* `{ runId, fetched, written }` — counters, not data — and this keeps that
|
|
465
|
+
* property while adding chaining.
|
|
466
|
+
*
|
|
467
|
+
* Every field here is O(1) in the size of the dataset. There is deliberately no
|
|
468
|
+
* column list, no sample and no schema: all three grow with the data or with the
|
|
469
|
+
* source's shape, and all three are recoverable by reading the stage itself.
|
|
470
|
+
*
|
|
471
|
+
* The rows live in the stage store ({@link CatalogStageStore}), addressed by
|
|
472
|
+
* `(runId, nodeId, batch)` — the same `(snapshot, batch)` addressing the
|
|
473
|
+
* warehouse already uses, and idempotent for the same reason: a retried step
|
|
474
|
+
* re-sends its batches and each one replaces itself rather than appending a
|
|
475
|
+
* second copy.
|
|
476
|
+
*/
|
|
477
|
+
export interface WorkflowStageRef {
|
|
478
|
+
runId: string;
|
|
479
|
+
nodeId: string;
|
|
480
|
+
/** Batches written, numbered 1..`batches`. Zero means the node produced nothing. */
|
|
481
|
+
batches: number;
|
|
482
|
+
rowCount: number;
|
|
483
|
+
}
|
|
484
|
+
/** What a node step receives. Ids and handles; never rows. */
|
|
485
|
+
export interface WorkflowNodeStepInput {
|
|
486
|
+
workflowId: string;
|
|
487
|
+
workflowVersion: number;
|
|
488
|
+
/** The connector run this belongs to, which is also the snapshot id. */
|
|
489
|
+
runId: string;
|
|
490
|
+
nodeId: string;
|
|
491
|
+
principalId: string;
|
|
492
|
+
/**
|
|
493
|
+
* The stages this node reads, in the order its inbound edges appear in the
|
|
494
|
+
* graph. Empty for a source node, which reads from a system instead.
|
|
495
|
+
*/
|
|
496
|
+
inputs: WorkflowStageRef[];
|
|
497
|
+
}
|
|
498
|
+
/** What a node step returns. Also ids and counters. */
|
|
499
|
+
export interface WorkflowNodeStepOutput {
|
|
500
|
+
nodeId: string;
|
|
501
|
+
/**
|
|
502
|
+
* Where this node put its rows, for its successors to read. Absent on a sink,
|
|
503
|
+
* which commits instead of staging.
|
|
504
|
+
*/
|
|
505
|
+
output?: WorkflowStageRef;
|
|
506
|
+
/** A sink's commit: the snapshot that became live, and its total row count. */
|
|
507
|
+
committed?: {
|
|
508
|
+
snapshotId: string;
|
|
509
|
+
rowCount: number;
|
|
510
|
+
};
|
|
511
|
+
/** Which transform version ran, for a transform node. */
|
|
512
|
+
transformVersion?: number;
|
|
513
|
+
rows: number;
|
|
514
|
+
elapsedMs: number;
|
|
515
|
+
/**
|
|
516
|
+
* The one thing here that is not a counter, and the one exception worth
|
|
517
|
+
* making: logs are what an operator reads when a node misbehaves, and a
|
|
518
|
+
* checkpoint that dropped them would send them back to re-running the load to
|
|
519
|
+
* find out what it said. Bounded on both axes — a caller must cap the count
|
|
520
|
+
* and the line length before returning, the way the connector runner already
|
|
521
|
+
* caps at fifty lines.
|
|
522
|
+
*/
|
|
523
|
+
logs: string[];
|
|
524
|
+
}
|
|
525
|
+
/** Every way a graph can be refused. Exported so a canvas can key off the code. */
|
|
526
|
+
export declare const WORKFLOW_ISSUE_CODES: readonly ["empty", "invalid-node-id", "duplicate-node-id", "edge-endpoint-missing", "self-edge", "duplicate-edge", "cycle", "no-source", "source-has-input", "no-sink", "duplicate-sink-type", "sink-has-output", "unreachable", "dead-end", "transform-not-named"];
|
|
527
|
+
export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
|
|
528
|
+
export interface WorkflowValidationIssue {
|
|
529
|
+
code: WorkflowIssueCode;
|
|
530
|
+
/**
|
|
531
|
+
* The nodes this is about. Always populated except for the whole-graph
|
|
532
|
+
* issues, because "this workflow is invalid" is not something anyone can act
|
|
533
|
+
* on — the message has to name the box to go and look at.
|
|
534
|
+
*/
|
|
535
|
+
nodeIds: string[];
|
|
536
|
+
/** Already a full sentence, addressed to whoever drew the graph. */
|
|
537
|
+
message: string;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Everything that makes a graph unrunnable, in one pure function.
|
|
541
|
+
*
|
|
542
|
+
* Pure and dependency-free on purpose, and exported from the browser entry point
|
|
543
|
+
* as well as the server one, so the canvas and the store run *the same*
|
|
544
|
+
* validator. A canvas that validates against its own copy of these rules and a
|
|
545
|
+
* server that validates against another is a canvas that eventually lies —
|
|
546
|
+
* either by refusing something the server would accept, or, far worse, by
|
|
547
|
+
* accepting something the server then rejects at run time, halfway through a
|
|
548
|
+
* load. The server still calls this itself: shared code is not the same as
|
|
549
|
+
* trusted input, and the store must refuse a graph that arrived by curl.
|
|
550
|
+
*
|
|
551
|
+
* Structural problems are reported alone. Reachability computed over edges that
|
|
552
|
+
* point at nodes which do not exist produces a second page of consequences, and
|
|
553
|
+
* burying the one real problem under them is how a validation message stops
|
|
554
|
+
* being read.
|
|
555
|
+
*/
|
|
556
|
+
export declare function validateWorkflow(graph: WorkflowGraph): WorkflowValidationIssue[];
|
|
557
|
+
/**
|
|
558
|
+
* The order the nodes run in, and the inputs each one gets.
|
|
559
|
+
*
|
|
560
|
+
* Here rather than in the runner because the wiring rules — a node runs after
|
|
561
|
+
* everything wired into it, and receives its inputs in edge order — are the same
|
|
562
|
+
* rules {@link validateWorkflow} enforces, and two implementations of one rule
|
|
563
|
+
* is how a graph that validated comes out executing differently.
|
|
564
|
+
*
|
|
565
|
+
* Throws on an invalid graph rather than returning a best effort. A partial
|
|
566
|
+
* order over a broken graph is a load that half-happens, which is harder to
|
|
567
|
+
* recover from than one that never started.
|
|
568
|
+
*/
|
|
569
|
+
export declare function workflowRunOrder(graph: WorkflowGraph): Array<{
|
|
570
|
+
node: WorkflowNode;
|
|
571
|
+
inputs: string[];
|
|
572
|
+
}>;
|
|
573
|
+
/**
|
|
574
|
+
* A stable fingerprint of what a graph *does*.
|
|
575
|
+
*
|
|
576
|
+
* Behaviour only: node ids, kinds, the configuration each kind executes on, and
|
|
577
|
+
* the edges in their order. Names and canvas positions are excluded, so moving a
|
|
578
|
+
* box or fixing a typo in a label does not bump the version — the same rule
|
|
579
|
+
* `saveTransform` already applies when it bumps only on a code change, and for
|
|
580
|
+
* the same reason. A version number inflated by cosmetic edits is useless for
|
|
581
|
+
* the one question it exists to answer.
|
|
582
|
+
*
|
|
583
|
+
* Nodes are sorted by id because their array order changes nothing; edges are
|
|
584
|
+
* deliberately *not* sorted, because their order decides what a node with
|
|
585
|
+
* several inputs receives.
|
|
586
|
+
*
|
|
587
|
+
* FNV-1a rather than a hash from `node:crypto`: this file is imported by the
|
|
588
|
+
* browser entry point, and it is change detection rather than a security
|
|
589
|
+
* primitive — nobody is defending against a chosen-collision attack on their own
|
|
590
|
+
* canvas. Two passes with different offsets are concatenated, which is enough
|
|
591
|
+
* spread that an accidental collision between two graphs of one workflow is not
|
|
592
|
+
* a thing to plan for.
|
|
593
|
+
*/
|
|
594
|
+
export declare function workflowGraphHash(graph: WorkflowGraph): string;
|
|
595
|
+
/**
|
|
596
|
+
* Narrow a stored node, loudly.
|
|
597
|
+
*
|
|
598
|
+
* Used when reading a graph back out of a JSON column, and it throws rather than
|
|
599
|
+
* skipping what it does not recognise for a reason specific to graphs: dropping
|
|
600
|
+
* an unknown node silently changes what the workflow does — the surrounding
|
|
601
|
+
* edges then point at nothing, or worse, the graph still validates and simply
|
|
602
|
+
* omits a step — and a load that quietly ran nine of ten nodes is far harder to
|
|
603
|
+
* notice than one that refused to start.
|
|
604
|
+
*/
|
|
605
|
+
export declare function isWorkflowNode(value: unknown): value is WorkflowNode;
|
|
606
|
+
export declare function isWorkflowEdge(value: unknown): value is WorkflowEdge;
|
|
607
|
+
/**
|
|
608
|
+
* Holding and serving workflows.
|
|
609
|
+
*
|
|
610
|
+
* Its own interface, mixed into {@link CatalogPipelineStore} as optional
|
|
611
|
+
* members, because a store may legitimately not have these — the routing proxy
|
|
612
|
+
* in the MikroORM package and any store written against the previous shape of
|
|
613
|
+
* this interface both implement `CatalogPipelineStore` today, and turning that
|
|
614
|
+
* into a compile error would be a breaking change for a feature that is
|
|
615
|
+
* additive. {@link supportsWorkflows} is how a caller asks, and "this deployment
|
|
616
|
+
* cannot hold workflows" is then a sentence a UI can say rather than a method
|
|
617
|
+
* that is missing at run time.
|
|
618
|
+
*/
|
|
619
|
+
export interface CatalogWorkflowStore {
|
|
620
|
+
listWorkflows(): Promise<CatalogWorkflow[]>;
|
|
621
|
+
getWorkflow(id: string): Promise<CatalogWorkflow | undefined>;
|
|
622
|
+
/**
|
|
623
|
+
* Validates before it writes, and refuses naming the node.
|
|
624
|
+
*
|
|
625
|
+
* `version`, `graphHash` and `targetType` are not inputs: the first two are
|
|
626
|
+
* derived from the graph and the third from the sink, and accepting them from
|
|
627
|
+
* a caller would let a client claim a version it did not produce.
|
|
628
|
+
*/
|
|
629
|
+
saveWorkflow(input: Pick<CatalogWorkflow, 'name' | 'nodes' | 'edges'> & {
|
|
630
|
+
id?: string;
|
|
631
|
+
description?: string;
|
|
632
|
+
}, createdBy: string): Promise<CatalogWorkflow>;
|
|
633
|
+
/** Refuses while any connector still runs it. */
|
|
634
|
+
deleteWorkflow(id: string): Promise<boolean>;
|
|
635
|
+
/** Which connectors run it. Named, so a refusal can say. */
|
|
636
|
+
connectorsUsingWorkflow(id: string): Promise<CatalogConnector[]>;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Where the rows between two nodes actually sit.
|
|
640
|
+
*
|
|
641
|
+
* Separate from {@link CatalogWorkflowStore} because they are different kinds of
|
|
642
|
+
* thing — one holds authored metadata, the other holds a run's intermediate
|
|
643
|
+
* data — and a deployment could reasonably keep the second somewhere the first
|
|
644
|
+
* is not, a columnar store or object storage rather than the catalog database.
|
|
645
|
+
*
|
|
646
|
+
* The rows cannot simply be staged in the target type's own table, which was the
|
|
647
|
+
* first thing tried: that table has the columns the *type* declares, and an
|
|
648
|
+
* intermediate node's rows are mid-transformation and generally have others. A
|
|
649
|
+
* write there would drop them, and the load would come out missing fields that
|
|
650
|
+
* the transform demonstrably produced.
|
|
651
|
+
*/
|
|
652
|
+
export interface CatalogStageStore {
|
|
653
|
+
/**
|
|
654
|
+
* Idempotent per `(runId, nodeId, batch)`, exactly like the warehouse's own
|
|
655
|
+
* `write`. A retried durable step re-sends its batches and each one replaces
|
|
656
|
+
* itself, so a retry cannot double a node's output.
|
|
657
|
+
*/
|
|
658
|
+
writeStage(input: {
|
|
659
|
+
runId: string;
|
|
660
|
+
nodeId: string;
|
|
661
|
+
/** 1-based, matching the batch numbering the connector runner already uses. */
|
|
662
|
+
batch: number;
|
|
663
|
+
rows: Array<Record<string, unknown>>;
|
|
664
|
+
}): Promise<{
|
|
665
|
+
written: number;
|
|
666
|
+
}>;
|
|
667
|
+
/** One batch at a time, so reading a stage never means holding all of it. */
|
|
668
|
+
readStage(ref: {
|
|
669
|
+
runId: string;
|
|
670
|
+
nodeId: string;
|
|
671
|
+
batch: number;
|
|
672
|
+
}): Promise<Array<Record<string, unknown>>>;
|
|
673
|
+
/**
|
|
674
|
+
* Drop everything a run staged. Called after the sink commits, and after a
|
|
675
|
+
* failed run has been given up on — intermediate rows are worth keeping only
|
|
676
|
+
* as long as something might still resume onto them.
|
|
677
|
+
*/
|
|
678
|
+
dropStages(runId: string): Promise<number>;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Whether this store can hold workflows at all.
|
|
682
|
+
*
|
|
683
|
+
* Checks the methods rather than a flag, the same way {@link isPipelineStore}
|
|
684
|
+
* does, because a flag is a claim and a method is the thing itself.
|
|
685
|
+
*/
|
|
686
|
+
export declare function supportsWorkflows(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogWorkflowStore;
|
|
687
|
+
export declare function supportsWorkflowStages(store: CatalogPipelineStore): store is CatalogPipelineStore & CatalogStageStore;
|
|
688
|
+
export interface CatalogPipelineStore extends Partial<CatalogWorkflowStore>, Partial<CatalogStageStore> {
|
|
689
|
+
listConnectors(): Promise<CatalogConnector[]>;
|
|
690
|
+
getConnector(id: string): Promise<CatalogConnector | undefined>;
|
|
691
|
+
saveConnector(input: Omit<CatalogConnector, 'id' | 'createdAt' | 'updatedAt' | 'createdBy'> & {
|
|
692
|
+
id?: string;
|
|
693
|
+
}, createdBy: string): Promise<CatalogConnector>;
|
|
694
|
+
deleteConnector(id: string): Promise<boolean>;
|
|
695
|
+
/**
|
|
696
|
+
* Record where a run got to.
|
|
697
|
+
*
|
|
698
|
+
* Its own method rather than part of `saveConnector`, so advancing a
|
|
699
|
+
* watermark can never carry an accidental edit to the query beside it.
|
|
700
|
+
*/
|
|
701
|
+
saveConnectorState(id: string, state: Record<string, unknown>): Promise<void>;
|
|
702
|
+
listConnections(): Promise<CatalogConnection[]>;
|
|
703
|
+
getConnection(id: string): Promise<CatalogConnection | undefined>;
|
|
704
|
+
saveConnection(input: Omit<CatalogConnection, 'id' | 'createdAt' | 'updatedAt' | 'createdBy'> & {
|
|
705
|
+
id?: string;
|
|
706
|
+
}, createdBy: string): Promise<CatalogConnection>;
|
|
707
|
+
/**
|
|
708
|
+
* Refuses while any connector still reads through it.
|
|
709
|
+
*
|
|
710
|
+
* Deleting one out from under its connectors would turn every one of them
|
|
711
|
+
* into a load that fails at run time with a missing address, discovered on a
|
|
712
|
+
* schedule rather than at the moment somebody decided.
|
|
713
|
+
*/
|
|
714
|
+
deleteConnection(id: string): Promise<boolean>;
|
|
715
|
+
recordConnectionCheck(id: string, check: ConnectionCheck): Promise<void>;
|
|
716
|
+
/** Which connectors read through a connection. Named, so a refusal can say. */
|
|
717
|
+
connectorsUsingConnection(id: string): Promise<CatalogConnector[]>;
|
|
718
|
+
listTransforms(): Promise<CatalogTransform[]>;
|
|
719
|
+
getTransform(id: string): Promise<CatalogTransform | undefined>;
|
|
720
|
+
saveTransform(input: Pick<CatalogTransform, 'name' | 'language' | 'code'> & {
|
|
721
|
+
id?: string;
|
|
722
|
+
description?: string;
|
|
723
|
+
}, createdBy: string): Promise<CatalogTransform>;
|
|
724
|
+
deleteTransform(id: string): Promise<boolean>;
|
|
725
|
+
startRun(input: {
|
|
726
|
+
connectorId: string;
|
|
727
|
+
snapshotId: string;
|
|
728
|
+
principalId: string;
|
|
729
|
+
/**
|
|
730
|
+
* Which graph is about to run, and how.
|
|
731
|
+
*
|
|
732
|
+
* Recorded at the *start* rather than at the finish, deliberately. A run
|
|
733
|
+
* that crashes hard enough never to reach `finishRun` still has to be
|
|
734
|
+
* traceable to the graph that was running, and a run row that only learns
|
|
735
|
+
* which workflow it was on the way out is exactly the row that will be
|
|
736
|
+
* missing it for the failure somebody is investigating. All optional, so a
|
|
737
|
+
* single-transform connector calls this precisely as it did before.
|
|
738
|
+
*/
|
|
739
|
+
workflowId?: string;
|
|
740
|
+
workflowVersion?: number;
|
|
741
|
+
graphHash?: string;
|
|
742
|
+
executionMode?: WorkflowExecutionMode;
|
|
743
|
+
}): Promise<ConnectorRun>;
|
|
744
|
+
finishRun(id: string, outcome: Partial<Pick<ConnectorRun, 'status' | 'fetched' | 'written' | 'logs' | 'error' | 'transformVersion' | 'nodeOutcomes'>>): Promise<ConnectorRun | undefined>;
|
|
745
|
+
listRuns(connectorId?: string, limit?: number): Promise<ConnectorRun[]>;
|
|
746
|
+
}
|
|
747
|
+
export declare const CATALOG_PIPELINE_STORE: unique symbol;
|
|
748
|
+
export declare function isPipelineStore(store: unknown): store is CatalogPipelineStore;
|
|
749
|
+
/**
|
|
750
|
+
* A source somebody can reach, named once and reused.
|
|
751
|
+
*
|
|
752
|
+
* Connectors used to carry their own URL and their own credential reference, so
|
|
753
|
+
* five connectors reading one database held five copies of the same
|
|
754
|
+
* configuration and moving that database meant editing five rows — with no way
|
|
755
|
+
* to find them, and no way to test the connection except by running a load.
|
|
756
|
+
*
|
|
757
|
+
* This is the shape Airflow calls a Connection and NiFi a Controller Service,
|
|
758
|
+
* and it is worth the extra concept for one reason above the rest: it gives
|
|
759
|
+
* "who reaches this system, and with whose credential" a single place to be
|
|
760
|
+
* answered.
|
|
761
|
+
*
|
|
762
|
+
* Credentials stay out of it, exactly as before. What is stored is the *name*
|
|
763
|
+
* of an environment variable, so a leaked catalog database gives away the shape
|
|
764
|
+
* of an integration rather than the keys to it. Encrypting a secret at rest
|
|
765
|
+
* here would mean owning a master key, its rotation and its blast radius, which
|
|
766
|
+
* is a larger promise than this service should make.
|
|
767
|
+
*/
|
|
768
|
+
export interface CatalogConnection {
|
|
769
|
+
id: string;
|
|
770
|
+
name: string;
|
|
771
|
+
description?: string;
|
|
772
|
+
/**
|
|
773
|
+
* Which kind of source it reaches.
|
|
774
|
+
*
|
|
775
|
+
* Deliberately the same vocabulary a connector uses: a connection to a
|
|
776
|
+
* database and a connector reading from one must agree about what they are
|
|
777
|
+
* talking to, and two vocabularies would let them disagree.
|
|
778
|
+
*/
|
|
779
|
+
kind: ConnectorKind;
|
|
780
|
+
/** Everything but the credential — host, bucket, endpoint, region. */
|
|
781
|
+
config: Record<string, unknown>;
|
|
782
|
+
/** Env var holding the credential, if reaching it needs one. */
|
|
783
|
+
secretEnvVar?: string;
|
|
784
|
+
createdBy: string;
|
|
785
|
+
createdAt: string;
|
|
786
|
+
updatedAt: string;
|
|
787
|
+
/** When it was last reached successfully, if it ever has been. */
|
|
788
|
+
lastCheckedAt?: string;
|
|
789
|
+
lastCheckOk?: boolean;
|
|
790
|
+
lastCheckError?: string;
|
|
791
|
+
}
|
|
792
|
+
/** What checking a connection found. */
|
|
793
|
+
export interface ConnectionCheck {
|
|
794
|
+
ok: boolean;
|
|
795
|
+
/** What was reached, in words — a server version, a bucket, a status code. */
|
|
796
|
+
detail: string;
|
|
797
|
+
elapsedMs: number;
|
|
798
|
+
error?: string;
|
|
799
|
+
}
|
|
800
|
+
export {};
|