@dudousxd/nestjs-catalog 0.15.0 → 0.16.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog.events.d.ts +14 -0
- package/dist/catalog.pipeline.d.ts +685 -71
- package/dist/catalog.pipeline.js +505 -10
- package/dist/client.d.ts +14 -4
- package/dist/client.js +61 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -3
- package/dist/transform-runner.d.ts +19 -1
- package/dist/transform-runner.js +63 -7
- package/package.json +6 -6
- package/LICENSE +0 -21
|
@@ -406,12 +406,18 @@ export interface CatalogTransform {
|
|
|
406
406
|
description?: string;
|
|
407
407
|
language: TransformLanguage;
|
|
408
408
|
/**
|
|
409
|
-
* The body of a function over one batch. It receives `records` and
|
|
410
|
-
* the rows to store.
|
|
409
|
+
* The body of a function over one batch. It receives `records` and
|
|
410
|
+
* `context`, and returns the rows to store.
|
|
411
411
|
*
|
|
412
412
|
* A batch rather than a record at a time: a transform that needs to look up,
|
|
413
413
|
* deduplicate or aggregate cannot do it one row at a time, and paying one
|
|
414
414
|
* process spawn per record would make any real load unusable.
|
|
415
|
+
*
|
|
416
|
+
* `context` is a {@link CatalogCodeContext} — the run, the node, the counts
|
|
417
|
+
* of what fed it, and the environment variables this deployment admits.
|
|
418
|
+
* Second rather than first, so that every transform written before it existed
|
|
419
|
+
* still runs: the harness supplies the parameter, and code that never names
|
|
420
|
+
* it is unaffected.
|
|
415
421
|
*/
|
|
416
422
|
code: string;
|
|
417
423
|
version: number;
|
|
@@ -460,6 +466,7 @@ export interface TransformResult {
|
|
|
460
466
|
export interface TransformRunner {
|
|
461
467
|
run(transform: Pick<CatalogTransform, 'language' | 'code'>, records: unknown[], options?: {
|
|
462
468
|
timeoutMs?: number;
|
|
469
|
+
context?: CatalogCodeContext;
|
|
463
470
|
}): Promise<TransformResult>;
|
|
464
471
|
/** Languages this runner can actually execute in this environment. */
|
|
465
472
|
available(): Promise<TransformLanguage[]>;
|
|
@@ -473,6 +480,123 @@ export interface TransformRunner {
|
|
|
473
480
|
pythonPackages?(): Promise<string[]>;
|
|
474
481
|
}
|
|
475
482
|
export declare const TRANSFORM_RUNNER: unique symbol;
|
|
483
|
+
/**
|
|
484
|
+
* The number in {@link CatalogCodeContext.contract}.
|
|
485
|
+
*
|
|
486
|
+
* Its own version, separate from {@link WORKFLOW_CALL_CONTRACT}, because the
|
|
487
|
+
* two travel to different places for different reasons — a call envelope
|
|
488
|
+
* crosses to another SDK over the durable wire, a code context crosses to a
|
|
489
|
+
* child process this repository spawned — and a transform written against one
|
|
490
|
+
* has to be able to say which it read.
|
|
491
|
+
*/
|
|
492
|
+
export declare const CODE_CONTEXT_CONTRACT = 1;
|
|
493
|
+
/**
|
|
494
|
+
* The second argument every code-bearing node's code is handed.
|
|
495
|
+
*
|
|
496
|
+
* ## Why this exists at all
|
|
497
|
+
*
|
|
498
|
+
* A transform is a function over a batch, and a batch is not the whole of what
|
|
499
|
+
* the code needs to know. It needs the credential for the API it enriches
|
|
500
|
+
* against; it needs to say which run it belongs to when it logs; and — the case
|
|
501
|
+
* that motivated this — a conditional node's predicate has no `records` at all
|
|
502
|
+
* and still has to answer "did the source return anything", which is the guard
|
|
503
|
+
* that stops an empty snapshot being committed over live data.
|
|
504
|
+
*
|
|
505
|
+
* ## Why it is not `process.env`
|
|
506
|
+
*
|
|
507
|
+
* The obvious answer to "code needs environment variables" is to stop trimming
|
|
508
|
+
* the child's environment. It is the wrong one, and this repository has already
|
|
509
|
+
* argued the case in `secret-env-allowlist.ts`: a `secretEnvVar` is chosen by
|
|
510
|
+
* whoever writes the connector, so `process.env[name]` with nothing in between
|
|
511
|
+
* let anyone holding `catalog:write` on one narrow object type read the host
|
|
512
|
+
* application's own `DATABASE_URL`. Transform code is *the same principal by a
|
|
513
|
+
* shorter route* — it is a string that principal saved, and it can print
|
|
514
|
+
* whatever it reads into `logs`, which land in the run record and are served at
|
|
515
|
+
* `catalog:read`.
|
|
516
|
+
*
|
|
517
|
+
* So {@link env} is the allow-listed environment and nothing else: the same
|
|
518
|
+
* policy, the same two levers, the same boot warning. One rule across the
|
|
519
|
+
* product rather than a second one that quietly undoes the first. What the
|
|
520
|
+
* child's *own* `process.env` holds is unchanged — still `{PATH, NODE_ENV}` —
|
|
521
|
+
* and the admitted values arrive as data on stdin instead.
|
|
522
|
+
*
|
|
523
|
+
* ## Replay
|
|
524
|
+
*
|
|
525
|
+
* Everything here is plain JSON, and everything except {@link env} and
|
|
526
|
+
* {@link environment} is derived from a durable step's checkpointed input: the
|
|
527
|
+
* run id, the node, the graph's version and the stage handles are byte-
|
|
528
|
+
* identical on every attempt and on every replay. Those two are reads of
|
|
529
|
+
* pod-local state, which for a transform is harmless — a transform runs inside
|
|
530
|
+
* a step whose *output* is checkpointed, so replay returns the answer rather
|
|
531
|
+
* than re-running the code — and for a predicate evaluated in a workflow body
|
|
532
|
+
* is not. A caller in that position must resolve the context inside a step and
|
|
533
|
+
* let the checkpoint carry it, so that a redeploy between the original run and
|
|
534
|
+
* the replay cannot move the branch. The shape is JSON precisely so that it can
|
|
535
|
+
* be.
|
|
536
|
+
*/
|
|
537
|
+
export interface CatalogCodeContext {
|
|
538
|
+
/** {@link CODE_CONTEXT_CONTRACT} at the time the code ran. */
|
|
539
|
+
contract: number;
|
|
540
|
+
/**
|
|
541
|
+
* The run this code belongs to, which is also the snapshot id.
|
|
542
|
+
*
|
|
543
|
+
* Absent means there is no run: the editor's try pane executes code against
|
|
544
|
+
* sample records and stores nothing. Anything derived from this — an
|
|
545
|
+
* idempotency key, a filename — should refuse rather than invent one, and the
|
|
546
|
+
* absence is the signal that lets it.
|
|
547
|
+
*/
|
|
548
|
+
runId?: string;
|
|
549
|
+
/** The graph, when the code is a node in one. Absent for a connector's single transform. */
|
|
550
|
+
workflow?: {
|
|
551
|
+
id: string;
|
|
552
|
+
name: string;
|
|
553
|
+
version: number;
|
|
554
|
+
};
|
|
555
|
+
/** The node, when the code is a node. Absent for a connector's single transform. */
|
|
556
|
+
node?: {
|
|
557
|
+
id: string;
|
|
558
|
+
name: string;
|
|
559
|
+
};
|
|
560
|
+
/** The connector, when the code runs as a connector's transform rather than in a graph. */
|
|
561
|
+
connectorId?: string;
|
|
562
|
+
/**
|
|
563
|
+
* The host's name for this copy of the world — `dev`, `prod` — when the host
|
|
564
|
+
* declared one.
|
|
565
|
+
*
|
|
566
|
+
* Nothing in the catalog can work this out for itself: a process may serve
|
|
567
|
+
* several environments and the choice arrives per request, so the only
|
|
568
|
+
* honest source is the host saying so. Present because branching on a named
|
|
569
|
+
* environment is legible in a way that sniffing a variable is not, and absent
|
|
570
|
+
* whenever the host stayed silent, which is a different thing from `dev`.
|
|
571
|
+
*/
|
|
572
|
+
environment?: string;
|
|
573
|
+
/**
|
|
574
|
+
* How many records reached this code.
|
|
575
|
+
*
|
|
576
|
+
* Redundant with `records.length` for a transform and the whole payload for a
|
|
577
|
+
* predicate, which has no records. Stated as one number because the line a
|
|
578
|
+
* predicate is written to hold is "did anything arrive", and making that a
|
|
579
|
+
* sum over {@link inputs} invites `inputs[0].rowCount`, which is wrong the
|
|
580
|
+
* moment somebody draws a second edge.
|
|
581
|
+
*/
|
|
582
|
+
rowCount: number;
|
|
583
|
+
/**
|
|
584
|
+
* Per inbound edge, in edge order — handles and counts, never the rows.
|
|
585
|
+
*
|
|
586
|
+
* The same {@link WorkflowStageRef} the call node hands a callee, deliberately
|
|
587
|
+
* rather than a second vocabulary for the same fact. Empty outside a graph.
|
|
588
|
+
*/
|
|
589
|
+
inputs: WorkflowStageRef[];
|
|
590
|
+
/**
|
|
591
|
+
* The environment variables this deployment admits, and only those.
|
|
592
|
+
*
|
|
593
|
+
* Filtered by the credential allow-list — see `secret-env-allowlist.ts` — so
|
|
594
|
+
* a name nobody admitted is not here, and an empty object is the ordinary
|
|
595
|
+
* answer on a deployment that has declared no policy. The run's logs say
|
|
596
|
+
* which it was.
|
|
597
|
+
*/
|
|
598
|
+
env: Record<string, string>;
|
|
599
|
+
}
|
|
476
600
|
export interface ConnectorRun {
|
|
477
601
|
id: string;
|
|
478
602
|
connectorId: string;
|
|
@@ -539,18 +663,60 @@ export interface ConnectorRun {
|
|
|
539
663
|
*/
|
|
540
664
|
nodeOutcomes?: Record<string, WorkflowNodeOutcome>;
|
|
541
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Why a `skipped` node did not run, when there is more to say than "the run
|
|
668
|
+
* stopped".
|
|
669
|
+
*
|
|
670
|
+
* **One entry, and the omission is the point.** `skipped` already meant one
|
|
671
|
+
* thing before branches existed — the run failed upstream and never reached
|
|
672
|
+
* this node — and every outcome ever stored records that meaning by *not*
|
|
673
|
+
* carrying a reason. Adding `run-stopped` to this list would not describe those
|
|
674
|
+
* rows, it would leave them describing an unknown reason, so the pre-existing
|
|
675
|
+
* meaning stays the absent one and this names only the new fact: the node is on
|
|
676
|
+
* a branch an {@link WorkflowIfNode} did not take.
|
|
677
|
+
*
|
|
678
|
+
* The distinction is not cosmetic. A sink skipped by a branch **committed
|
|
679
|
+
* nothing and left the live snapshot alone**, which is a correct, successful
|
|
680
|
+
* outcome; a sink skipped by a failure is part of a load that went wrong. A run
|
|
681
|
+
* panel that rendered both as "did not run" would answer "why is there no data
|
|
682
|
+
* in X" with the same shrug in both cases.
|
|
683
|
+
*/
|
|
684
|
+
export declare const WORKFLOW_SKIP_REASONS: readonly ["branch-not-taken"];
|
|
685
|
+
export type WorkflowSkipReason = (typeof WORKFLOW_SKIP_REASONS)[number];
|
|
686
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
687
|
+
export declare function isWorkflowSkipReason(value: unknown): value is WorkflowSkipReason;
|
|
542
688
|
/** What one node did during a run. Small by construction — counters, not rows. */
|
|
543
689
|
export interface WorkflowNodeOutcome {
|
|
544
690
|
/**
|
|
545
691
|
* `skipped` exists for the nodes downstream of a failure. Without it, a
|
|
546
692
|
* ten-node graph that died at node seven records three nodes with no entry at
|
|
547
693
|
* all, which reads the same as three nodes nobody has looked at yet.
|
|
694
|
+
*
|
|
695
|
+
* It now carries a second, legitimate meaning as well — a node on the branch
|
|
696
|
+
* an `if` did not take — and {@link skippedBecause} is what tells the two
|
|
697
|
+
* apart.
|
|
548
698
|
*/
|
|
549
699
|
status: 'succeeded' | 'failed' | 'skipped';
|
|
550
700
|
/** Rows this node produced, or committed if it is the sink. */
|
|
551
701
|
rows: number;
|
|
552
702
|
/** For a transform node: which version of its code ran. */
|
|
553
703
|
transformVersion?: number;
|
|
704
|
+
/**
|
|
705
|
+
* For an {@link WorkflowIfNode}: the branch this run took.
|
|
706
|
+
*
|
|
707
|
+
* **Written once, on the first evaluation, and read back on every replay.**
|
|
708
|
+
* This is the record the durable path replays from rather than re-evaluating:
|
|
709
|
+
* a predicate reads the environment, and an environment is pod-local, so a
|
|
710
|
+
* replay landing on another pod could otherwise take the other branch halfway
|
|
711
|
+
* through a run and load through a shape nobody chose. See
|
|
712
|
+
* `WorkflowRunnerService.runIf`.
|
|
713
|
+
*
|
|
714
|
+
* It is also the answer to "why did nothing load into X", which is the
|
|
715
|
+
* question a branch makes askable and nothing else on a run can answer.
|
|
716
|
+
*/
|
|
717
|
+
branch?: WorkflowBranchLabel;
|
|
718
|
+
/** See {@link WORKFLOW_SKIP_REASONS}. Absent means the run stopped short. */
|
|
719
|
+
skippedBecause?: WorkflowSkipReason;
|
|
554
720
|
elapsedMs?: number;
|
|
555
721
|
error?: string;
|
|
556
722
|
}
|
|
@@ -572,9 +738,12 @@ export interface WorkflowNodeOutcome {
|
|
|
572
738
|
* It needs no new execution path, only a different body, and adding the kind
|
|
573
739
|
* would mean two ways to drop rows and two places to look when rows go
|
|
574
740
|
* missing.
|
|
575
|
-
* - **branch / split** — already expressible
|
|
576
|
-
* read by both successors, each of which
|
|
577
|
-
* nothing for
|
|
741
|
+
* - **branch / split (unconditional)** — already expressible, and still is: a
|
|
742
|
+
* node with two outbound edges is read by both successors, each of which
|
|
743
|
+
* filters differently. There is nothing for an *unconditional* split to do.
|
|
744
|
+
* {@link WorkflowIfNode} is the conditional one, and it earns its kind by
|
|
745
|
+
* doing something no wiring can express — deciding that one of those
|
|
746
|
+
* successors, and everything only it feeds, does not run at all.
|
|
578
747
|
* - **merge / join** — a node with several inbound edges receives its inputs
|
|
579
748
|
* concatenated in edge order (see {@link WorkflowEdge}). A keyed join is then
|
|
580
749
|
* ordinary code inside the transform, which can already see every record.
|
|
@@ -591,10 +760,28 @@ export interface WorkflowNodeOutcome {
|
|
|
591
760
|
* outside a run, which is why `call` names one and not a step. If a step is
|
|
592
761
|
* what you want, the thing to call is a one-step workflow wrapping it.
|
|
593
762
|
*/
|
|
594
|
-
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call"];
|
|
763
|
+
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if"];
|
|
595
764
|
export type WorkflowNodeKind = (typeof WORKFLOW_NODE_KINDS)[number];
|
|
596
765
|
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
597
766
|
export declare function isWorkflowNodeKind(value: unknown): value is WorkflowNodeKind;
|
|
767
|
+
/**
|
|
768
|
+
* The kind that never compiles quietly.
|
|
769
|
+
*
|
|
770
|
+
* Every place that decides something *per kind* ends in a call to this, so a
|
|
771
|
+
* kind added to {@link WORKFLOW_NODE_KINDS} without a branch there is a type
|
|
772
|
+
* error naming the file rather than a graph that saves, validates, draws and
|
|
773
|
+
* then does the wrong thing. This codebase has been bitten by exactly that
|
|
774
|
+
* shape — a `toGraph` branch forgetting a field, a node-kind map missing a kind
|
|
775
|
+
* — and the fix each time was to make the omission impossible rather than to
|
|
776
|
+
* remember harder.
|
|
777
|
+
*
|
|
778
|
+
* It throws as well as failing to compile, because the narrowing that reaches
|
|
779
|
+
* it is over data that arrives as JSON: a node whose `kind` passed
|
|
780
|
+
* {@link isWorkflowNodeKind} in an older build and reaches a newer one is
|
|
781
|
+
* possible, and returning a default for it would be the silent path this exists
|
|
782
|
+
* to close.
|
|
783
|
+
*/
|
|
784
|
+
export declare function unreachableNodeKind(node: never, where: string): never;
|
|
598
785
|
/**
|
|
599
786
|
* The longest a node id may be, and the alphabet it may use.
|
|
600
787
|
*
|
|
@@ -618,12 +805,68 @@ interface WorkflowNodeBase {
|
|
|
618
805
|
* a person arranged and the server then forgot is a canvas that loses work.
|
|
619
806
|
* Excluded from the graph fingerprint for the same reason a rename is: moving
|
|
620
807
|
* a box is not a new version of the graph.
|
|
808
|
+
*
|
|
809
|
+
* A position is in the units {@link WORKFLOW_NODE_WIDTH} is measured in, and
|
|
810
|
+
* anything **generating** one should space it with {@link workflowColumnX} and
|
|
811
|
+
* {@link workflowRowY} rather than by picking a number.
|
|
621
812
|
*/
|
|
622
813
|
position?: {
|
|
623
814
|
x: number;
|
|
624
815
|
y: number;
|
|
625
816
|
};
|
|
626
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* How big a node is on the canvas, and therefore how far apart two of them have
|
|
820
|
+
* to be.
|
|
821
|
+
*
|
|
822
|
+
* ## Why the server owns a number about pixels
|
|
823
|
+
*
|
|
824
|
+
* Because the server *writes positions*. `adoptConnector` lays a connector out
|
|
825
|
+
* as a graph, and anything else that mints a graph without a person drawing it —
|
|
826
|
+
* a promotion, a template, a script — has the same job. A writer that does not
|
|
827
|
+
* know how wide a node is can only guess, and the guess was wrong: adoption used
|
|
828
|
+
* to place columns 220 apart against a node 224 wide, so every adopted graph
|
|
829
|
+
* drew each box overlapping the next by four pixels. Nothing was stacked and
|
|
830
|
+
* nothing was missing, so it read as boxes glued together rather than as a bug,
|
|
831
|
+
* and it survived until somebody opened thirteen of them.
|
|
832
|
+
*
|
|
833
|
+
* The fix is not a bigger number. 220 was not too small, it was **derived from
|
|
834
|
+
* nothing** — it had no relationship to the width it was supposed to clear, so
|
|
835
|
+
* it was only ever correct by luck and would go wrong again the next time the
|
|
836
|
+
* node's styling changed. These constants are the relationship, stated once, in
|
|
837
|
+
* the one package both the writer and the canvas already depend on.
|
|
838
|
+
*
|
|
839
|
+
* ## What pins them to the drawing
|
|
840
|
+
*
|
|
841
|
+
* {@link WORKFLOW_NODE_WIDTH} is not a description of the node — it is the
|
|
842
|
+
* *source of* the node's width. `WorkflowNodeBody` in
|
|
843
|
+
* `@dudousxd/nestjs-catalog-react` sets its own width from this constant rather
|
|
844
|
+
* than from a Tailwind class, so the two cannot drift: changing this changes the
|
|
845
|
+
* box, and there is no second number to forget.
|
|
846
|
+
*/
|
|
847
|
+
export declare const WORKFLOW_NODE_WIDTH = 224;
|
|
848
|
+
/** How tall a node is. The other half of {@link WORKFLOW_NODE_WIDTH}'s contract. */
|
|
849
|
+
export declare const WORKFLOW_NODE_HEIGHT = 80;
|
|
850
|
+
/**
|
|
851
|
+
* Clear space between one column and the next, on top of the node's own width.
|
|
852
|
+
*
|
|
853
|
+
* Wide enough for the edge between two nodes to be read as a line with a
|
|
854
|
+
* direction rather than as a join. This is the part that is taste; the width it
|
|
855
|
+
* is added to is not.
|
|
856
|
+
*/
|
|
857
|
+
export declare const WORKFLOW_COLUMN_GAP = 96;
|
|
858
|
+
/** Clear space between two nodes sharing a column. */
|
|
859
|
+
export declare const WORKFLOW_ROW_GAP = 32;
|
|
860
|
+
/**
|
|
861
|
+
* The x of the nth column, counting from zero.
|
|
862
|
+
*
|
|
863
|
+
* Every generator of a layout goes through this rather than multiplying by a
|
|
864
|
+
* literal, which is what makes "columns never overlap" a property of one
|
|
865
|
+
* function instead of a coincidence repeated at each call site.
|
|
866
|
+
*/
|
|
867
|
+
export declare function workflowColumnX(column: number): number;
|
|
868
|
+
/** The y of the nth row within a column, counting from zero. */
|
|
869
|
+
export declare function workflowRowY(row: number): number;
|
|
627
870
|
/**
|
|
628
871
|
* Reads records out of a system.
|
|
629
872
|
*
|
|
@@ -743,13 +986,229 @@ export interface WorkflowCallNode extends WorkflowNodeBase {
|
|
|
743
986
|
/** Parameters the author typed, handed to the child under `input`. */
|
|
744
987
|
config: Record<string, unknown>;
|
|
745
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* The kinds of test an {@link WorkflowIfNode} can make.
|
|
991
|
+
*
|
|
992
|
+
* A second predicate shape was always going to arrive — the note on
|
|
993
|
+
* {@link WorkflowIfNode} says so about `code` — and the shape it arrives into is
|
|
994
|
+
* the decision worth arguing about, because the alternative was to keep both
|
|
995
|
+
* tests' fields flat on the node and mark them optional. That version types a
|
|
996
|
+
* gate as "an env var, maybe, and a threshold, maybe": a node carrying both is
|
|
997
|
+
* representable, a node carrying neither is representable, and every reader has
|
|
998
|
+
* to invent its own rule for which one wins. It is the same mistake the note on
|
|
999
|
+
* {@link WorkflowNode} refuses for node kinds, one level down.
|
|
1000
|
+
*
|
|
1001
|
+
* So the predicate is a union with a discriminant of its own, and every decision
|
|
1002
|
+
* made per predicate kind ends in {@link unreachablePredicateKind} — a third
|
|
1003
|
+
* shape is then a build failure listing the files that have to answer for it,
|
|
1004
|
+
* rather than a gate that saves, draws, and quietly always takes the `else`.
|
|
1005
|
+
*/
|
|
1006
|
+
export declare const WORKFLOW_PREDICATE_KINDS: readonly ["env", "rowCount"];
|
|
1007
|
+
export type WorkflowPredicateKind = (typeof WORKFLOW_PREDICATE_KINDS)[number];
|
|
1008
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
1009
|
+
export declare function isWorkflowPredicateKind(value: unknown): value is WorkflowPredicateKind;
|
|
1010
|
+
/**
|
|
1011
|
+
* {@link unreachableNodeKind}, one level down, and for the identical reason.
|
|
1012
|
+
*
|
|
1013
|
+
* Every branch over {@link WorkflowIfPredicate} ends here, so a predicate kind
|
|
1014
|
+
* added to the list without a rule for hashing it, validating it or evaluating
|
|
1015
|
+
* it is a type error naming the file — not a graph that runs and decides
|
|
1016
|
+
* something nobody authored. It throws as well, because predicates arrive as
|
|
1017
|
+
* JSON out of a column and a build older than the data is a thing that happens.
|
|
1018
|
+
*/
|
|
1019
|
+
export declare function unreachablePredicateKind(predicate: never, where: string): never;
|
|
1020
|
+
/**
|
|
1021
|
+
* Tests a variable on the machine that runs the load.
|
|
1022
|
+
*
|
|
1023
|
+
* The original predicate, and the one the node was built for: a deployment that
|
|
1024
|
+
* has a ClickHouse has its URL and one that does not has nothing. It names the
|
|
1025
|
+
* variable and never its value — see the note on {@link WorkflowIfNode} for why
|
|
1026
|
+
* that is a safety property rather than a convenience.
|
|
1027
|
+
*/
|
|
1028
|
+
export interface WorkflowEnvPredicate {
|
|
1029
|
+
kind: 'env';
|
|
1030
|
+
/**
|
|
1031
|
+
* The name of the environment variable to read on the machine that runs the
|
|
1032
|
+
* node. **The name, never the value** — nothing about a credential is stored
|
|
1033
|
+
* in a catalog.
|
|
1034
|
+
*/
|
|
1035
|
+
envVar: string;
|
|
1036
|
+
/**
|
|
1037
|
+
* What it has to equal for the `then` branch to be taken.
|
|
1038
|
+
*
|
|
1039
|
+
* Absent means "is it set to anything non-empty", which is the ClickHouse
|
|
1040
|
+
* case: a deployment that has one has the URL, a deployment that does not has
|
|
1041
|
+
* nothing. Present means an exact string comparison, which is the
|
|
1042
|
+
* `DEPLOY_ENV = local` case.
|
|
1043
|
+
*/
|
|
1044
|
+
equals?: string;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Tests how many rows reached the gate.
|
|
1048
|
+
*
|
|
1049
|
+
* ## The case
|
|
1050
|
+
*
|
|
1051
|
+
* "Only run the sink if the source returned anything." A nightly export that
|
|
1052
|
+
* comes back empty because the upstream system is mid-maintenance is not a
|
|
1053
|
+
* failure — nothing is broken, there is simply nothing to load — but committing
|
|
1054
|
+
* it repoints the live view of a type at an empty snapshot, and the run reports
|
|
1055
|
+
* success while doing it. A gate in front of the sink turns that into a skip,
|
|
1056
|
+
* and a skipped node is never executed, so nothing commits. That is the same
|
|
1057
|
+
* guarantee the `else` branch already gives, pointed at the case that actually
|
|
1058
|
+
* happens.
|
|
1059
|
+
*
|
|
1060
|
+
* ## Which rows
|
|
1061
|
+
*
|
|
1062
|
+
* The ones on the single inbound edge. `validateWorkflow` refuses a gate with
|
|
1063
|
+
* more than one (`if-needs-one-input`) and refuses one with none as unreachable,
|
|
1064
|
+
* so "how many rows" has exactly one answer — and it is the count on the very
|
|
1065
|
+
* {@link WorkflowStageRef} the gate hands on, so the number tested and the rows
|
|
1066
|
+
* carried cannot disagree.
|
|
1067
|
+
*
|
|
1068
|
+
* ## Where the number comes from, which is the replay argument
|
|
1069
|
+
*
|
|
1070
|
+
* `WorkflowStageRef.rowCount`, off {@link WorkflowNodeStepInput.inputs}, which
|
|
1071
|
+
* is part of the step's checkpointed input and was itself produced by an
|
|
1072
|
+
* upstream step's checkpointed output. Nothing counts rows at evaluation time
|
|
1073
|
+
* and nothing reads the stage store: a resumed run on another pod sees the same
|
|
1074
|
+
* number the first attempt saw, and the branch it produced is read back off
|
|
1075
|
+
* {@link WorkflowNodeStepOutput.branch} anyway.
|
|
1076
|
+
*
|
|
1077
|
+
* ## Why a threshold and not "greater than zero"
|
|
1078
|
+
*
|
|
1079
|
+
* `atLeast: 1` *is* "did anything arrive", so the common case costs nothing to
|
|
1080
|
+
* express — and "a full export is never under ten thousand rows, so treat a
|
|
1081
|
+
* hundred as a broken upstream rather than as data" is the next thing anybody
|
|
1082
|
+
* asks for, and it would otherwise need a second predicate kind for one integer.
|
|
1083
|
+
*
|
|
1084
|
+
* One comparison and one direction, deliberately. `atMost`, `equals` and a
|
|
1085
|
+
* chosen operator were all considered and are all the same mistake the node's
|
|
1086
|
+
* own `negate` flag would have been: the inverse test is already expressible by
|
|
1087
|
+
* swapping which successor is on `then` and which is on `else`, and two ways to
|
|
1088
|
+
* say one thing is two places to look when a load takes the branch nobody
|
|
1089
|
+
* expected.
|
|
1090
|
+
*/
|
|
1091
|
+
export interface WorkflowRowCountPredicate {
|
|
1092
|
+
kind: 'rowCount';
|
|
1093
|
+
/**
|
|
1094
|
+
* How many rows have to reach the gate for the `then` branch to be taken.
|
|
1095
|
+
*
|
|
1096
|
+
* A whole number of at least one, and `validateWorkflow` says so. Zero is
|
|
1097
|
+
* refused rather than treated as "always" because it is a gate that can only
|
|
1098
|
+
* ever answer one way — the `else` subtree would never run on any deployment,
|
|
1099
|
+
* which is the silent half-graph this node's whole design is arranged against.
|
|
1100
|
+
*/
|
|
1101
|
+
atLeast: number;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* What an `if` node tests. See {@link WORKFLOW_PREDICATE_KINDS}.
|
|
1105
|
+
*/
|
|
1106
|
+
export type WorkflowIfPredicate = WorkflowEnvPredicate | WorkflowRowCountPredicate;
|
|
1107
|
+
/**
|
|
1108
|
+
* Narrow a stored predicate, for the same reason {@link isWorkflowNode} narrows
|
|
1109
|
+
* a stored node: it arrives as JSON out of a column, and a gate read back
|
|
1110
|
+
* without its test is a gate that has to invent one.
|
|
1111
|
+
*
|
|
1112
|
+
* A row count that is not a whole number — `NaN` from a JSON round trip of an
|
|
1113
|
+
* unparsed field, an `Infinity` that serialised as `null` — is refused rather
|
|
1114
|
+
* than kept, because every comparison against it is false and the symptom is a
|
|
1115
|
+
* `then` branch that silently never runs again.
|
|
1116
|
+
*/
|
|
1117
|
+
export declare function isWorkflowIfPredicate(value: unknown): value is WorkflowIfPredicate;
|
|
1118
|
+
/**
|
|
1119
|
+
* Sends the rows down one of its two outbound branches, and skips the other.
|
|
1120
|
+
*
|
|
1121
|
+
* The node the maintainer asked for, and the reason it is a node rather than a
|
|
1122
|
+
* flag on an edge: a deployment that has a ClickHouse and a deployment that does
|
|
1123
|
+
* not are the *same graph*, and the difference between them is one decision made
|
|
1124
|
+
* at run time. Modelling it as two workflows means two things to keep in step;
|
|
1125
|
+
* modelling it as an edge flag means the decision has nowhere to be recorded and
|
|
1126
|
+
* nothing on the canvas to open.
|
|
1127
|
+
*
|
|
1128
|
+
* ## What it does to the rows: nothing
|
|
1129
|
+
*
|
|
1130
|
+
* An `if` is a gate, not a stage. It passes its input through untouched — its
|
|
1131
|
+
* output ref *is* its input's ref — so a branch costs no copy of the dataset and
|
|
1132
|
+
* no second write into the stage store. That is also why it takes **exactly one
|
|
1133
|
+
* inbound edge**: a node hands its successors one {@link WorkflowStageRef}, so a
|
|
1134
|
+
* gate fed by two inputs could only either copy the rows to merge them (paying
|
|
1135
|
+
* for the whole dataset to make a decision that reads none of it) or silently
|
|
1136
|
+
* drop one. Merging is what a transform is for; put one in front.
|
|
1137
|
+
*
|
|
1138
|
+
* That holds for {@link WorkflowRowCountPredicate} too, which is the one
|
|
1139
|
+
* predicate that sounds like it reads the data and does not: the count it tests
|
|
1140
|
+
* is the number already written on the {@link WorkflowStageRef} it was handed,
|
|
1141
|
+
* so a gate still touches no rows — and with one inbound edge there is exactly
|
|
1142
|
+
* one count for "how many rows" to mean.
|
|
1143
|
+
*
|
|
1144
|
+
* ## The predicate is declarative, and that is the safety property
|
|
1145
|
+
*
|
|
1146
|
+
* It is one of {@link WORKFLOW_PREDICATE_KINDS} — a variable's name, or a count
|
|
1147
|
+
* off a checkpoint — and never a value and never code. Three reasons, in order
|
|
1148
|
+
* of how much they cost to get wrong:
|
|
1149
|
+
*
|
|
1150
|
+
* 1. **Replay.** The durable engine replays a run, possibly on another pod. A
|
|
1151
|
+
* predicate is by definition the thing whose answer decides which half of the
|
|
1152
|
+
* graph exists, so an answer that can differ between the run and its replay
|
|
1153
|
+
* is a run that loads through a shape nobody chose — and it would show up as
|
|
1154
|
+
* a non-determinism error two nodes later, naming neither the branch nor the
|
|
1155
|
+
* variable. The outcome is therefore recorded on first evaluation
|
|
1156
|
+
* ({@link WorkflowNodeOutcome.branch}) and read back afterwards, and the
|
|
1157
|
+
* declarative form is what keeps that record small enough to be a checkpoint.
|
|
1158
|
+
* Both predicate kinds are answerable from what a step was handed:
|
|
1159
|
+
* {@link WorkflowRowCountPredicate} reads a number that arrived on the step's
|
|
1160
|
+
* own checkpointed input rather than counting anything.
|
|
1161
|
+
* 2. **A predicate is not a place for a secret.** A name is stored; a value
|
|
1162
|
+
* never is. This is the same rule {@link WorkflowSourceNode.secretEnvVar}
|
|
1163
|
+
* follows and for the same reason.
|
|
1164
|
+
* 3. **Code would need a context to read, and this node does not own it.** What
|
|
1165
|
+
* code-bearing nodes may see — allow-listed variables, a `catalog` object —
|
|
1166
|
+
* is a question being answered elsewhere. A `code` predicate is additive the
|
|
1167
|
+
* day it lands: it becomes another member of {@link WorkflowIfPredicate}, and
|
|
1168
|
+
* everything that reads a branch reads it off the recorded outcome exactly as
|
|
1169
|
+
* it does now.
|
|
1170
|
+
*
|
|
1171
|
+
* To invert the test, swap which successor is on `then` and which is on `else`.
|
|
1172
|
+
* There is deliberately no `negate` flag: two ways to say one thing is two
|
|
1173
|
+
* places to look when a load takes the branch you did not expect.
|
|
1174
|
+
*/
|
|
1175
|
+
export interface WorkflowIfNode extends WorkflowNodeBase {
|
|
1176
|
+
kind: 'if';
|
|
1177
|
+
/**
|
|
1178
|
+
* What it tests. A union rather than a field per test — see
|
|
1179
|
+
* {@link WORKFLOW_PREDICATE_KINDS} for why that is the whole point.
|
|
1180
|
+
*/
|
|
1181
|
+
predicate: WorkflowIfPredicate;
|
|
1182
|
+
}
|
|
746
1183
|
/**
|
|
747
1184
|
* A discriminated union, so narrowing a node is `node.kind === "sink"` and
|
|
748
1185
|
* never a type assertion. This is why the kind list is not simply a string on
|
|
749
1186
|
* one node shape with every field optional: that shape lets a source node carry
|
|
750
1187
|
* a `transformId` and nothing catches it.
|
|
751
1188
|
*/
|
|
752
|
-
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode;
|
|
1189
|
+
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode;
|
|
1190
|
+
/**
|
|
1191
|
+
* Which side of an {@link WorkflowIfNode} a wire leaves by.
|
|
1192
|
+
*
|
|
1193
|
+
* **Two closed values rather than free-form labels**, which was the other design
|
|
1194
|
+
* and is worse in the one way that matters here: an unlabelled branch is a
|
|
1195
|
+
* branch that never runs, and a *misspelled* branch is one too. With a free
|
|
1196
|
+
* string, `thn` is a subtree that silently never executes and a graph that
|
|
1197
|
+
* validates perfectly; with these, it is refused at the boundary and is a type
|
|
1198
|
+
* error in the console. The failure a branch introduces is "nothing loaded and
|
|
1199
|
+
* nothing complained", so the vocabulary is the place to make it impossible.
|
|
1200
|
+
*
|
|
1201
|
+
* An N-way `switch` node was considered and is deliberately not this: it would
|
|
1202
|
+
* have to carry its own case list plus a default, the cases would have to be
|
|
1203
|
+
* validated against the labels, and an unmatched value would need a rule. That
|
|
1204
|
+
* is a different node, and it can be added later without changing this one —
|
|
1205
|
+
* because a boolean question is what a predicate answers, and an `if` is exactly
|
|
1206
|
+
* the shape of a boolean question.
|
|
1207
|
+
*/
|
|
1208
|
+
export declare const WORKFLOW_BRANCH_LABELS: readonly ["then", "else"];
|
|
1209
|
+
export type WorkflowBranchLabel = (typeof WORKFLOW_BRANCH_LABELS)[number];
|
|
1210
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
1211
|
+
export declare function isWorkflowBranchLabel(value: unknown): value is WorkflowBranchLabel;
|
|
753
1212
|
/**
|
|
754
1213
|
* One wire.
|
|
755
1214
|
*
|
|
@@ -766,6 +1225,22 @@ export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | Workflow
|
|
|
766
1225
|
export interface WorkflowEdge {
|
|
767
1226
|
from: string;
|
|
768
1227
|
to: string;
|
|
1228
|
+
/**
|
|
1229
|
+
* Which branch of an {@link WorkflowIfNode} this wire leaves by.
|
|
1230
|
+
*
|
|
1231
|
+
* **Optional, and absent on every edge that existed before branches did.** An
|
|
1232
|
+
* edge with no label is a plain wire that always carries rows when its source
|
|
1233
|
+
* ran — which is what every stored edge is, so nothing about an existing graph
|
|
1234
|
+
* changes, including its fingerprint. The label is *required* on an edge
|
|
1235
|
+
* leaving an `if` and *refused* on any other, both by `validateWorkflow`: a
|
|
1236
|
+
* label on a wire nothing branches at would look like a decision and be
|
|
1237
|
+
* ignored, and an unlabelled wire out of an `if` has no branch to belong to.
|
|
1238
|
+
*
|
|
1239
|
+
* Several wires may share a label. An `if` whose `then` side fans out to two
|
|
1240
|
+
* nodes is ordinary fan-out that happens to be conditional; what is refused is
|
|
1241
|
+
* two wires between the *same pair*, which was already refused as a duplicate.
|
|
1242
|
+
*/
|
|
1243
|
+
branch?: WorkflowBranchLabel;
|
|
769
1244
|
}
|
|
770
1245
|
/** Just the executable part of a workflow, for validating a canvas draft. */
|
|
771
1246
|
export interface WorkflowGraph {
|
|
@@ -987,6 +1462,17 @@ export interface WorkflowNodeStepOutput {
|
|
|
987
1462
|
};
|
|
988
1463
|
/** Which transform version ran, for a transform node. */
|
|
989
1464
|
transformVersion?: number;
|
|
1465
|
+
/**
|
|
1466
|
+
* Which branch an `if` node took, for an `if` node.
|
|
1467
|
+
*
|
|
1468
|
+
* **On the step's output, which is the whole mechanism.** A step's output is
|
|
1469
|
+
* what the durable engine checkpoints and hands back on replay without running
|
|
1470
|
+
* the step again, so putting the evaluated branch here means the decision is
|
|
1471
|
+
* made exactly once, in the run's own history, and every later turn reads the
|
|
1472
|
+
* recorded one. A body that asked the predicate itself would be re-evaluating
|
|
1473
|
+
* a pod-local fact on a pod that may not be the same one.
|
|
1474
|
+
*/
|
|
1475
|
+
branch?: WorkflowBranchLabel;
|
|
990
1476
|
rows: number;
|
|
991
1477
|
elapsedMs: number;
|
|
992
1478
|
/**
|
|
@@ -1076,35 +1562,127 @@ export declare function readWorkflowCallOutput(value: unknown): WorkflowCallOutp
|
|
|
1076
1562
|
/**
|
|
1077
1563
|
* One workflow a call node could name.
|
|
1078
1564
|
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1565
|
+
* ## What this used to say, and what changed
|
|
1566
|
+
*
|
|
1567
|
+
* This shape was declared with nothing producing it, because a picker needs a
|
|
1568
|
+
* list and no list existed. The durable engine could answer
|
|
1569
|
+
* `workflowBody(name, version)` for the process asking and nothing more, and a
|
|
1570
|
+
* missing body is ambiguous by construction: it equally means "not registered",
|
|
1571
|
+
* "registered through `registerRemote` against another SDK", or "a group
|
|
1572
|
+
* resolved by convention against a live worker". A list inferred from that
|
|
1573
|
+
* would have omitted precisely the cross-SDK workflows this node exists to
|
|
1574
|
+
* call, and would have omitted them silently.
|
|
1575
|
+
*
|
|
1576
|
+
* `@dudousxd/nestjs-durable-core` **0.65.0** closed that with
|
|
1577
|
+
* `WorkflowEngine.announcedWorkflows()`: live workers publish what they can
|
|
1578
|
+
* execute on the worker-descriptor keyspace, and every pod folds the same
|
|
1579
|
+
* published statements, so the answer no longer depends on which replica
|
|
1580
|
+
* served the request. That is what fills this shape now — see
|
|
1581
|
+
* `WorkflowLauncher.callableWorkflows` in `@dudousxd/nestjs-catalog-pipeline`
|
|
1582
|
+
* for the adapter, and keep three properties of the aggregate in mind, because
|
|
1583
|
+
* every field below exists to carry one of them:
|
|
1584
|
+
*
|
|
1585
|
+
* - **It is what is runnable, not what is known about.** The announcer is
|
|
1586
|
+
* always the process that CONSUMES the queue, so a `registerRemote` entry is
|
|
1587
|
+
* never announced by the engine that declared it. An operator running bodies
|
|
1588
|
+
* inline with no queue announces nothing at all — its workflows are real and
|
|
1589
|
+
* startable, and simply not in this list.
|
|
1590
|
+
* - **It is a snapshot.** An announcement lives on a descriptor key with the
|
|
1591
|
+
* worker-heartbeat TTL, so a worker that dies takes its entries with it
|
|
1592
|
+
* within about one beat. Nothing built on this should be cached past that,
|
|
1593
|
+
* and nothing should present it as authoritative-forever.
|
|
1594
|
+
* - **Disagreements are kept, not merged.** Two live workers may claim the same
|
|
1595
|
+
* `name@version` from different groups. The aggregate refuses to pick a
|
|
1596
|
+
* winner, and so does this shape: see {@link disagreements}.
|
|
1597
|
+
*
|
|
1598
|
+
* One entry per **version**, never per name — a picker that listed names and
|
|
1599
|
+
* resolved the version for you would undo the pin the call node exists for.
|
|
1092
1600
|
*/
|
|
1093
1601
|
export interface CallableWorkflowRef {
|
|
1094
1602
|
name: string;
|
|
1095
|
-
|
|
1603
|
+
/**
|
|
1604
|
+
* The version to pin, and **absent is a real answer**.
|
|
1605
|
+
*
|
|
1606
|
+
* A worker that has not been upgraded announces a bare name with no version
|
|
1607
|
+
* and no group. Silence is not a claim, so no version is invented for it from
|
|
1608
|
+
* another announcer's — and an entry without one cannot satisfy the pin, which
|
|
1609
|
+
* is why {@link callableWorkflowBlock} refuses it rather than letting a picker
|
|
1610
|
+
* offer a name that would run whatever is newest on the day it runs.
|
|
1611
|
+
*/
|
|
1612
|
+
version?: string;
|
|
1096
1613
|
/** What it does, if the deployment publishes one. Shown beside the name. */
|
|
1097
1614
|
description?: string;
|
|
1098
1615
|
/**
|
|
1099
|
-
* The worker group its turns are dispatched to, when
|
|
1100
|
-
* that says "this one's body is not in this
|
|
1101
|
-
* a separate TS worker — which is precisely
|
|
1102
|
-
* tell from a missing body.
|
|
1616
|
+
* The worker group its turns are dispatched to, when the live announcers
|
|
1617
|
+
* agree on exactly one. The signal that says "this one's body is not in this
|
|
1618
|
+
* process" — a Python workflow, or a separate TS worker — which is precisely
|
|
1619
|
+
* what a caller cannot otherwise tell from a missing body.
|
|
1620
|
+
*
|
|
1621
|
+
* Absent means either nobody stated one or the announcers disagree, and those
|
|
1622
|
+
* two are not the same: the second puts a `group` entry in
|
|
1623
|
+
* {@link disagreements} and the first does not.
|
|
1103
1624
|
*/
|
|
1104
1625
|
group?: string;
|
|
1626
|
+
/**
|
|
1627
|
+
* How many live workers announce it. `1` is a single point of failure, and it
|
|
1628
|
+
* is never `0` — an entry exists only because somebody announced it.
|
|
1629
|
+
*/
|
|
1630
|
+
workers?: number;
|
|
1631
|
+
/**
|
|
1632
|
+
* The axes the live announcers do not agree on, empty or absent when they
|
|
1633
|
+
* speak with one voice. Carried rather than resolved: the registry refuses to
|
|
1634
|
+
* guess and so does everything downstream of it.
|
|
1635
|
+
*/
|
|
1636
|
+
disagreements?: CallableWorkflowDisagreement[];
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* One axis on which the live announcers of a workflow differ.
|
|
1640
|
+
*
|
|
1641
|
+
* Mirrors the durable engine's own `Disagreement` rather than re-deriving it,
|
|
1642
|
+
* and `values` holds every distinct **declared** value: an announcer that stated
|
|
1643
|
+
* nothing on the axis contributes nothing, because silence is not a claim.
|
|
1644
|
+
*/
|
|
1645
|
+
export interface CallableWorkflowDisagreement {
|
|
1646
|
+
axis: 'group' | 'origin' | 'requires';
|
|
1647
|
+
values: string[];
|
|
1105
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Why an announced entry must not be committed onto a call node, or `undefined`
|
|
1651
|
+
* when it can be.
|
|
1652
|
+
*
|
|
1653
|
+
* Pure and exported from the browser entry point as well as this one, for the
|
|
1654
|
+
* reason {@link validateWorkflow} is: the canvas that greys the option out and
|
|
1655
|
+
* anything server-side that reasons about the same list must apply *the same*
|
|
1656
|
+
* rule. A picker with its own copy of it is a picker that eventually offers
|
|
1657
|
+
* something the rest of the system considers unusable.
|
|
1658
|
+
*
|
|
1659
|
+
* Two refusals, and they are refusals rather than warnings because in both cases
|
|
1660
|
+
* committing the entry would write a node whose meaning nobody can state:
|
|
1661
|
+
*
|
|
1662
|
+
* - `no-version` — an un-upgraded worker announced a bare name. The call node's
|
|
1663
|
+
* whole point is the pin; a node holding a name and no version follows
|
|
1664
|
+
* whatever gets deployed next, which is the failure the version field exists
|
|
1665
|
+
* to prevent. The name is still perfectly typeable by hand *with* a version
|
|
1666
|
+
* the author knows, so this refuses the one-click commit and not the workflow.
|
|
1667
|
+
* - `ambiguous-group` — two live workers claim this exact `name@version` from
|
|
1668
|
+
* different groups. Two groups means two queues, and nothing here can know
|
|
1669
|
+
* which one a run would land on, so the two bodies may not even be the same
|
|
1670
|
+
* code. Picking one on the author's behalf would be acting on a claim nobody
|
|
1671
|
+
* made.
|
|
1672
|
+
*
|
|
1673
|
+
* A disagreement on `origin` or `requires` is deliberately **not** a refusal. It
|
|
1674
|
+
* is worth showing — two packages declaring one name is a mess somebody should
|
|
1675
|
+
* clean up — but it does not change which queue the run goes to, and refusing on
|
|
1676
|
+
* it would block a pin that is otherwise exactly determined.
|
|
1677
|
+
*/
|
|
1678
|
+
export interface CallableWorkflowBlock {
|
|
1679
|
+
code: 'no-version' | 'ambiguous-group';
|
|
1680
|
+
/** A full sentence, addressed to whoever is looking at the picker. */
|
|
1681
|
+
message: string;
|
|
1682
|
+
}
|
|
1683
|
+
export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
|
|
1106
1684
|
/** Every way a graph can be refused. Exported so a canvas can key off the code. */
|
|
1107
|
-
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", "call-not-named"];
|
|
1685
|
+
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", "call-not-named", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge"];
|
|
1108
1686
|
export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
|
|
1109
1687
|
export interface WorkflowValidationIssue {
|
|
1110
1688
|
code: WorkflowIssueCode;
|
|
@@ -1147,10 +1725,68 @@ export declare function validateWorkflow(graph: WorkflowGraph): WorkflowValidati
|
|
|
1147
1725
|
* order over a broken graph is a load that half-happens, which is harder to
|
|
1148
1726
|
* recover from than one that never started.
|
|
1149
1727
|
*/
|
|
1150
|
-
export declare function workflowRunOrder(graph: WorkflowGraph):
|
|
1728
|
+
export declare function workflowRunOrder(graph: WorkflowGraph): WorkflowRunOrderEntry[];
|
|
1729
|
+
/** One position in the order: which node, what feeds it, and on which branches. */
|
|
1730
|
+
export interface WorkflowRunOrderEntry {
|
|
1151
1731
|
node: WorkflowNode;
|
|
1732
|
+
/** Upstream node ids, in inbound-edge order. */
|
|
1152
1733
|
inputs: string[];
|
|
1153
|
-
|
|
1734
|
+
/**
|
|
1735
|
+
* The branch label of each inbound edge that has one, keyed by the upstream
|
|
1736
|
+
* node id.
|
|
1737
|
+
*
|
|
1738
|
+
* A record rather than an array positionally aligned with {@link inputs},
|
|
1739
|
+
* which was the obvious shape and is unsafe for one specific reason: this
|
|
1740
|
+
* travels through a durable checkpoint as JSON, and `JSON.stringify` turns an
|
|
1741
|
+
* `undefined` hole in an array into `null`. A plain wire would come back from
|
|
1742
|
+
* a replay as `null` rather than absent, `null !== 'then'` would be false, and
|
|
1743
|
+
* the node would be treated as dead — the graph would silently stop running
|
|
1744
|
+
* half of itself on resumed runs only. A key that is simply not there
|
|
1745
|
+
* round-trips exactly. The key is unique because duplicate edges are refused.
|
|
1746
|
+
*/
|
|
1747
|
+
inputBranches: Record<string, WorkflowBranchLabel>;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Whether a node runs, given what the nodes before it did.
|
|
1751
|
+
*
|
|
1752
|
+
* ## The rule
|
|
1753
|
+
*
|
|
1754
|
+
* A node runs when **at least one wire into it is live**, where a wire is live
|
|
1755
|
+
* if its source ran and — when the wire carries a branch label — the source is
|
|
1756
|
+
* an `if` that took that branch. A node with no inbound wires always runs, which
|
|
1757
|
+
* is every source and every originating call.
|
|
1758
|
+
*
|
|
1759
|
+
* ## Why the obvious rule is wrong
|
|
1760
|
+
*
|
|
1761
|
+
* The naive version is "mark everything downstream of the untaken edge as
|
|
1762
|
+
* skipped", and it is wrong on the shape branches are most often drawn in:
|
|
1763
|
+
*
|
|
1764
|
+
* ```
|
|
1765
|
+
* ┌ then → A ┐
|
|
1766
|
+
* if ──┤ ├→ C → sink
|
|
1767
|
+
* └ else → B ┘
|
|
1768
|
+
* ```
|
|
1769
|
+
*
|
|
1770
|
+
* `C` is downstream of `B`. Take the `then` branch and the naive rule walks from
|
|
1771
|
+
* the untaken `else` edge, reaches `B`, reaches `C`, and skips it — so the sink
|
|
1772
|
+
* never runs and the load silently commits nothing, on a graph whose whole
|
|
1773
|
+
* purpose was that both branches converge. Reachability from the **taken** edges
|
|
1774
|
+
* gets it right: `C` is reached through `A`, so it runs, and `B` — reached only
|
|
1775
|
+
* through the untaken edge — does not. `C` then sees an empty stage ref for `B`,
|
|
1776
|
+
* which is exactly what `stageRefsFor` already does for an upstream that
|
|
1777
|
+
* produced nothing, so the positions a merge reads stay aligned with the wires
|
|
1778
|
+
* that were drawn.
|
|
1779
|
+
*
|
|
1780
|
+
* It is evaluated incrementally rather than as a graph walk because the answers
|
|
1781
|
+
* arrive as the run goes: `workflowRunOrder` is topological, so by the time a
|
|
1782
|
+
* node is reached every node feeding it has an outcome. That also means this
|
|
1783
|
+
* reads **only** what was recorded — no predicate is re-evaluated here, which is
|
|
1784
|
+
* the property the whole branch feature rests on.
|
|
1785
|
+
*/
|
|
1786
|
+
export declare function workflowNodeRuns(entry: {
|
|
1787
|
+
inputs: readonly string[];
|
|
1788
|
+
inputBranches?: Readonly<Record<string, WorkflowBranchLabel>>;
|
|
1789
|
+
}, outcomes: Readonly<Record<string, WorkflowNodeOutcome>>): boolean;
|
|
1154
1790
|
/**
|
|
1155
1791
|
* A stable fingerprint of what a graph *does*.
|
|
1156
1792
|
*
|
|
@@ -1313,49 +1949,6 @@ export interface CatalogWorkflowStore {
|
|
|
1313
1949
|
schedule?: string;
|
|
1314
1950
|
enabled?: boolean;
|
|
1315
1951
|
}, changedBy: string): Promise<CatalogWorkflow>;
|
|
1316
|
-
/**
|
|
1317
|
-
* Wrap a connector that predates workflows into the graph it always was.
|
|
1318
|
-
*
|
|
1319
|
-
* **The upgrade path, and the reason removing `POST connectors` does not
|
|
1320
|
-
* orphan anything.** A deployment upgrading into this change has connectors
|
|
1321
|
-
* that were authored directly — a kind, a config, optionally one transform,
|
|
1322
|
-
* and a target type — and no route can edit them any more. Three outcomes
|
|
1323
|
-
* were possible: migrate them, leave them readable but frozen, or make
|
|
1324
|
-
* somebody rebuild each by hand. This is the first, and it is chosen because
|
|
1325
|
-
* the shape was already exactly representable: a connector *is* a
|
|
1326
|
-
* single-source, single-sink graph with at most one transform in the middle,
|
|
1327
|
-
* and it has been describable as one since `workflowId` existed.
|
|
1328
|
-
*
|
|
1329
|
-
* ## What is preserved, and why each matters
|
|
1330
|
-
*
|
|
1331
|
-
* - **The connector id.** It is the mutex key, the owner of every
|
|
1332
|
-
* `ConnectorRun` and the holder of the watermark. Minting a fresh connector
|
|
1333
|
-
* and leaving the old one would split one pipeline's history in two and
|
|
1334
|
-
* leave nothing serialising the halves against each other.
|
|
1335
|
-
* - **The watermark, re-keyed.** A plain connector kept a flat `state`; a
|
|
1336
|
-
* graph keys it by source-node id. Carrying the blob over unchanged would
|
|
1337
|
-
* leave the new source node with no watermark at all, and the first run
|
|
1338
|
-
* after the upgrade would re-read an incremental source from the beginning —
|
|
1339
|
-
* which is not data loss, but is a surprise measured in hours on a large
|
|
1340
|
-
* source.
|
|
1341
|
-
* - **The schedule**, moved to the graph, which is where it is authored now.
|
|
1342
|
-
*
|
|
1343
|
-
* ## What changes, stated plainly
|
|
1344
|
-
*
|
|
1345
|
-
* The graph is published as `ready` without a person having declared it
|
|
1346
|
-
* finished, because refusing to publish would leave the pipeline not running —
|
|
1347
|
-
* an upgrade that silently stops twelve loads is a worse outcome than one that
|
|
1348
|
-
* carries a decision forward on the operator's behalf. It is validated first
|
|
1349
|
-
* and the adoption is **refused** if the wrap does not validate, so nothing is
|
|
1350
|
-
* published that could not have been drawn.
|
|
1351
|
-
*
|
|
1352
|
-
* Idempotent: a connector that already has a `workflowId` is left alone and
|
|
1353
|
-
* answered `undefined`. That is what makes it safe to run at every boot.
|
|
1354
|
-
*/
|
|
1355
|
-
adoptConnector(connectorId: string, adoptedBy: string): Promise<{
|
|
1356
|
-
workflow: CatalogWorkflow;
|
|
1357
|
-
connector: CatalogConnector;
|
|
1358
|
-
} | undefined>;
|
|
1359
1952
|
}
|
|
1360
1953
|
/**
|
|
1361
1954
|
* Where the rows between two nodes actually sit.
|
|
@@ -1612,6 +2205,27 @@ export interface CatalogConnection {
|
|
|
1612
2205
|
lastCheckOk?: boolean;
|
|
1613
2206
|
lastCheckError?: string;
|
|
1614
2207
|
}
|
|
2208
|
+
/**
|
|
2209
|
+
* What a redacted password reads as, on the wire.
|
|
2210
|
+
*
|
|
2211
|
+
* Declared here rather than in `@dudousxd/nestjs-catalog-pipeline`, where the
|
|
2212
|
+
* redaction itself lives, because this literal is not an implementation detail
|
|
2213
|
+
* of the redaction: it is part of what `GET pipeline/connections` answers, and
|
|
2214
|
+
* a browser is the audience it was invented for. A form that lets somebody
|
|
2215
|
+
* paste an address has to be able to recognise the string it was shown — a
|
|
2216
|
+
* `url` whose password is exactly this came out of a read, and posting it back
|
|
2217
|
+
* as a NEW connection stores the word "REDACTED" as the password. There is no
|
|
2218
|
+
* stored row behind a create for `restoreRedactedSecrets` to put the real one
|
|
2219
|
+
* back from, so nothing downstream can catch it: the row saves, and the failure
|
|
2220
|
+
* arrives at the first scheduled load as an authentication error against a
|
|
2221
|
+
* password nobody typed.
|
|
2222
|
+
*
|
|
2223
|
+
* The pipeline package re-exports this rather than declaring its own, so the
|
|
2224
|
+
* two halves cannot drift. A fixed literal rather than a run of asterisks, so
|
|
2225
|
+
* it is greppable in a bug report and cannot be mistaken for a password
|
|
2226
|
+
* somebody actually chose.
|
|
2227
|
+
*/
|
|
2228
|
+
export declare const REDACTED_SECRET = "REDACTED";
|
|
1615
2229
|
/** What checking a connection found. */
|
|
1616
2230
|
export interface ConnectionCheck {
|
|
1617
2231
|
ok: boolean;
|