@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/catalog.controller.d.ts +8 -0
  4. package/dist/catalog.controller.js +482 -0
  5. package/dist/catalog.decorators.d.ts +37 -0
  6. package/dist/catalog.decorators.js +50 -0
  7. package/dist/catalog.environment.d.ts +442 -0
  8. package/dist/catalog.environment.js +645 -0
  9. package/dist/catalog.events.d.ts +179 -0
  10. package/dist/catalog.events.js +110 -0
  11. package/dist/catalog.module.d.ts +5 -0
  12. package/dist/catalog.module.js +71 -0
  13. package/dist/catalog.options.d.ts +79 -0
  14. package/dist/catalog.options.js +4 -0
  15. package/dist/catalog.overlay-store.d.ts +25 -0
  16. package/dist/catalog.overlay-store.js +44 -0
  17. package/dist/catalog.overlay-store.token.d.ts +1 -0
  18. package/dist/catalog.overlay-store.token.js +4 -0
  19. package/dist/catalog.pipeline.d.ts +800 -0
  20. package/dist/catalog.pipeline.js +606 -0
  21. package/dist/catalog.principal.d.ts +209 -0
  22. package/dist/catalog.principal.js +245 -0
  23. package/dist/catalog.query-cache.d.ts +25 -0
  24. package/dist/catalog.query-cache.js +0 -0
  25. package/dist/catalog.query.d.ts +76 -0
  26. package/dist/catalog.query.js +64 -0
  27. package/dist/catalog.registry.base.d.ts +21 -0
  28. package/dist/catalog.registry.base.js +17 -0
  29. package/dist/catalog.registry.d.ts +44 -0
  30. package/dist/catalog.registry.js +359 -0
  31. package/dist/catalog.service.d.ts +115 -0
  32. package/dist/catalog.service.js +366 -0
  33. package/dist/catalog.store.d.ts +419 -0
  34. package/dist/catalog.store.js +175 -0
  35. package/dist/catalog.types.d.ts +165 -0
  36. package/dist/catalog.types.js +19 -0
  37. package/dist/catalog.workspace.d.ts +426 -0
  38. package/dist/catalog.workspace.js +87 -0
  39. package/dist/client.d.ts +86 -0
  40. package/dist/client.js +83 -0
  41. package/dist/index.d.ts +19 -0
  42. package/dist/index.js +109 -0
  43. package/dist/stores/mikro-orm-read.store.d.ts +20 -0
  44. package/dist/stores/mikro-orm-read.store.js +120 -0
  45. package/dist/transform-runner.d.ts +54 -0
  46. package/dist/transform-runner.js +280 -0
  47. 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 {};