@dudousxd/nestjs-catalog 0.26.0 → 0.28.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.aggregate.d.ts +376 -0
- package/dist/catalog.aggregate.js +703 -0
- package/dist/catalog.pipeline.d.ts +786 -10
- package/dist/catalog.pipeline.js +1188 -63
- package/dist/client.d.ts +2 -2
- package/dist/client.js +49 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +49 -3
- package/package.json +1 -1
|
@@ -1183,12 +1183,25 @@ export interface WorkflowNodeOutcome {
|
|
|
1183
1183
|
* {@link WorkflowIfNode} is the conditional one, and it earns its kind by
|
|
1184
1184
|
* doing something no wiring can express — deciding that one of those
|
|
1185
1185
|
* successors, and everything only it feeds, does not run at all.
|
|
1186
|
-
* - **merge
|
|
1187
|
-
* concatenated in edge order (see {@link WorkflowEdge})
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1186
|
+
* - **merge (unkeyed)** — a node with several inbound edges already receives its
|
|
1187
|
+
* inputs concatenated in edge order (see {@link WorkflowEdge}), so a `merge`
|
|
1188
|
+
* kind would be a box that draws what the wires already say. It would also
|
|
1189
|
+
* have had to carry a strategy field whose values the runner implements one by
|
|
1190
|
+
* one, and an unimplemented strategy in a dropdown is the failure this list
|
|
1191
|
+
* exists to avoid.
|
|
1192
|
+
* - **join (keyed)** — *this half used to be refused with the entry above, and
|
|
1193
|
+
* the reversal is left visible rather than edited out*, the way the `filter`
|
|
1194
|
+
* entry leaves its own. The old argument was that a keyed join is ordinary
|
|
1195
|
+
* code inside a transform, which can already see every record. Every word of
|
|
1196
|
+
* that is true and it is exactly the problem: "can already see every record"
|
|
1197
|
+
* is the same sentence as "holds the whole load", and it is why a transform
|
|
1198
|
+
* makes `ConnectorRunnerService` log *"Held all N records in memory"*. A join
|
|
1199
|
+
* does not need both sides held. It needs **one** side held — as a map, keyed
|
|
1200
|
+
* — while the other streams past it, and that asymmetry is a property of the
|
|
1201
|
+
* operation that a function over a batch cannot express and a runner therefore
|
|
1202
|
+
* cannot exploit. {@link WorkflowLookupNode} is the keyed half, built narrow:
|
|
1203
|
+
* one key, named enrichment fields, and a reference side that is bounded and
|
|
1204
|
+
* refused loudly rather than held quietly.
|
|
1192
1205
|
* - **call a durable *step*** — the sibling of {@link WorkflowCallNode} that
|
|
1193
1206
|
* somebody will eventually come looking for, and it cannot be built. A
|
|
1194
1207
|
* durable step has no global identity: it is dispatched by a routing name
|
|
@@ -1199,7 +1212,7 @@ export interface WorkflowNodeOutcome {
|
|
|
1199
1212
|
* outside a run, which is why `call` names one and not a step. If a step is
|
|
1200
1213
|
* what you want, the thing to call is a one-step workflow wrapping it.
|
|
1201
1214
|
*/
|
|
1202
|
-
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter", "rename"];
|
|
1215
|
+
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter", "rename", "aggregate", "lookup"];
|
|
1203
1216
|
export type WorkflowNodeKind = (typeof WORKFLOW_NODE_KINDS)[number];
|
|
1204
1217
|
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
1205
1218
|
export declare function isWorkflowNodeKind(value: unknown): value is WorkflowNodeKind;
|
|
@@ -2340,13 +2353,730 @@ export declare function renameColumnRefusals(columns: Record<string, string>): s
|
|
|
2340
2353
|
* map, and the values are checked one by one rather than trusted from the type.
|
|
2341
2354
|
*/
|
|
2342
2355
|
export declare function isWorkflowRenameColumns(value: unknown): value is Record<string, string>;
|
|
2356
|
+
/**
|
|
2357
|
+
* The aggregate functions this node computes, and the rule that closes the list.
|
|
2358
|
+
*
|
|
2359
|
+
* A closed list with an exhaustiveness guard, for the reason every other list in
|
|
2360
|
+
* this file is one. What is different here is that the list has a **stated
|
|
2361
|
+
* admission rule**, because "we will keep it narrow" is a promise nobody can
|
|
2362
|
+
* check and a rule is:
|
|
2363
|
+
*
|
|
2364
|
+
* > A function is in if it can be computed from a **fixed-size accumulator**,
|
|
2365
|
+
* > and if its answer does not depend on a decision the config would have to
|
|
2366
|
+
* > carry.
|
|
2367
|
+
*
|
|
2368
|
+
* The first half is the node's whole reason to exist. A hash aggregate is cheap
|
|
2369
|
+
* because it holds one entry per group; an accumulator whose size grows with the
|
|
2370
|
+
* number of *rows* in a group puts the rows back in memory and gives up the
|
|
2371
|
+
* property. The second half is what keeps the config from becoming a small
|
|
2372
|
+
* language: a function that needs an extra field to say what it means is a
|
|
2373
|
+
* function whose meaning was not decided.
|
|
2374
|
+
*
|
|
2375
|
+
* What the rule excludes, so the omissions are on the record rather than
|
|
2376
|
+
* implied:
|
|
2377
|
+
*
|
|
2378
|
+
* - **`countDistinct`** — the sharpest one. It needs a set of the distinct
|
|
2379
|
+
* values *per group per column*, so its accumulator is O(distinct values) and
|
|
2380
|
+
* a high-cardinality column inside a group holds the load. It is the exact
|
|
2381
|
+
* thing this node was built to stop doing, wearing an aggregate's name. A
|
|
2382
|
+
* sketch (HyperLogLog) is fixed-size and is a different function — an
|
|
2383
|
+
* estimate — which is not something to ship under the word `distinct`.
|
|
2384
|
+
* - **`median`, percentiles, `stddev` of a stream** — all need the values, or a
|
|
2385
|
+
* digest that is an approximation with an error bound the config would have to
|
|
2386
|
+
* carry.
|
|
2387
|
+
* - **`first` / `last`** — fixed-size, and excluded on the other half of the
|
|
2388
|
+
* rule: they mean "in input order", and this node's input order is a
|
|
2389
|
+
* `SELECT` without an `ORDER BY`. An aggregate that returns a different value
|
|
2390
|
+
* on a rerun is a load nobody can diff. `min`/`max` are the order-independent
|
|
2391
|
+
* version and are what somebody reaching for `first` usually wants.
|
|
2392
|
+
* - **Conditional aggregation — `MAX(CASE WHEN … THEN … END)`** — deliberately
|
|
2393
|
+
* out of scope, and it is the one omission a reader of flip's `wo` query will
|
|
2394
|
+
* go looking for, because that query has a three-branch status ladder in it.
|
|
2395
|
+
* Admitting it means admitting a predicate *inside* an aggregate, which is a
|
|
2396
|
+
* second expression language nested in the first, evaluated per row per
|
|
2397
|
+
* aggregate. That is transform territory and the generic
|
|
2398
|
+
* {@link WorkflowTransformNode} still exists. What the ladder actually is, is
|
|
2399
|
+
* a priority ordering over a closed set of codes, and it composes: map the
|
|
2400
|
+
* code to a rank in a transform above this node, `min` the rank, map it back
|
|
2401
|
+
* below. Two cheap per-record steps instead of a language.
|
|
2402
|
+
* - **`avg` is in**, and it is in *because* of the rule rather than despite it.
|
|
2403
|
+
* It is `sum` and `count` in one accumulator, both of which are already here,
|
|
2404
|
+
* and SQL has exactly one answer for it. Excluding it would have made the list
|
|
2405
|
+
* an arbitrary set that happened to cover one query, which is the thing the
|
|
2406
|
+
* rule is for.
|
|
2407
|
+
*/
|
|
2408
|
+
export declare const WORKFLOW_AGGREGATE_FUNCTIONS: readonly ["count", "sum", "avg", "min", "max", "join"];
|
|
2409
|
+
export type WorkflowAggregateFunction = (typeof WORKFLOW_AGGREGATE_FUNCTIONS)[number];
|
|
2410
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
2411
|
+
export declare function isWorkflowAggregateFunction(value: unknown): value is WorkflowAggregateFunction;
|
|
2412
|
+
/**
|
|
2413
|
+
* {@link unreachableNodeKind}, one level down, and for the identical reason.
|
|
2414
|
+
*
|
|
2415
|
+
* Every branch over {@link WorkflowAggregateFunction} ends here, so a seventh
|
|
2416
|
+
* function added to the list without an accumulator, a finisher, a canonical
|
|
2417
|
+
* form and a sentence is a type error naming the file rather than a node that
|
|
2418
|
+
* saves, draws and then computes nothing. It throws as well, because a function
|
|
2419
|
+
* name arrives as JSON out of a column and a build older than the data is a
|
|
2420
|
+
* thing that happens.
|
|
2421
|
+
*/
|
|
2422
|
+
export declare function unreachableAggregateFunction(fn: never, where: string): never;
|
|
2423
|
+
/**
|
|
2424
|
+
* What happens to a driving row whose key matches no reference row.
|
|
2425
|
+
*
|
|
2426
|
+
* Three words rather than a boolean, because the three are the three joins SQL
|
|
2427
|
+
* has names for and each is a different node. See
|
|
2428
|
+
* {@link WorkflowLookupNode.unmatched} for which one to reach for.
|
|
2429
|
+
*/
|
|
2430
|
+
export declare const WORKFLOW_LOOKUP_UNMATCHED: readonly ["null", "drop", "fail"];
|
|
2431
|
+
export type WorkflowLookupUnmatched = (typeof WORKFLOW_LOOKUP_UNMATCHED)[number];
|
|
2432
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
2433
|
+
export declare function isWorkflowLookupUnmatched(value: unknown): value is WorkflowLookupUnmatched;
|
|
2434
|
+
/**
|
|
2435
|
+
* The exhaustiveness guard for {@link WORKFLOW_LOOKUP_UNMATCHED}.
|
|
2436
|
+
*
|
|
2437
|
+
* {@link unreachableNodeKind}, one level down, and for the identical reason: the
|
|
2438
|
+
* three words decide whether a row keeps its data, disappears from the load, or
|
|
2439
|
+
* stops the run, and a fourth added without a branch would silently pick
|
|
2440
|
+
* whichever the last `if` was — which here means silently changing which rows
|
|
2441
|
+
* reach a published type.
|
|
2442
|
+
*/
|
|
2443
|
+
export declare function unreachableLookupUnmatched(value: never, where: string): never;
|
|
2444
|
+
/**
|
|
2445
|
+
* How many columns one node may group on.
|
|
2446
|
+
*
|
|
2447
|
+
* The same argument {@link WORKFLOW_RENAME_MAX_COLUMNS} makes, plus one specific
|
|
2448
|
+
* to this node: every extra group-by column can only ever *increase* the number
|
|
2449
|
+
* of groups, so a long list is the shape a high-cardinality grouping arrives in.
|
|
2450
|
+
* flip's real derivation groups on two.
|
|
2451
|
+
*/
|
|
2452
|
+
export declare const WORKFLOW_AGGREGATE_MAX_GROUP_BY = 16;
|
|
2453
|
+
/**
|
|
2454
|
+
* How many aggregates one node may compute.
|
|
2455
|
+
*
|
|
2456
|
+
* flip's `wo` derivation has 49, so the bound has to be comfortably above that
|
|
2457
|
+
* or the node does not do the job it was written for. Past a few hundred the
|
|
2458
|
+
* thing being expressed is a table definition rather than a summary, and the
|
|
2459
|
+
* cost is real: every aggregate is an accumulator held **per group**, so this
|
|
2460
|
+
* number multiplies {@link WORKFLOW_AGGREGATE_MAX_GROUPS} in the heap.
|
|
2461
|
+
*/
|
|
2462
|
+
export declare const WORKFLOW_AGGREGATE_MAX_AGGREGATES = 256;
|
|
2463
|
+
/**
|
|
2464
|
+
* The default ceiling on distinct groups, and the loud refusal that goes with
|
|
2465
|
+
* it.
|
|
2466
|
+
*
|
|
2467
|
+
* A hash aggregate is cheap **only while the groups are far fewer than the
|
|
2468
|
+
* rows**. Group on a near-unique column and it holds one accumulator row per
|
|
2469
|
+
* input row, which is the whole-batch behaviour this node replaces, arrived at
|
|
2470
|
+
* by a different route and with nothing on the canvas to point at. So the
|
|
2471
|
+
* ceiling exists, it is crossed loudly, and the message names the columns being
|
|
2472
|
+
* grouped on — because a bound that is merely reported is a bound that is
|
|
2473
|
+
* discovered by the machine running out of memory.
|
|
2474
|
+
*
|
|
2475
|
+
* A million is chosen against the measurement rather than as a round number:
|
|
2476
|
+
* flip's derivation holds 16,119, so the default is 62× the real case and no
|
|
2477
|
+
* author of a sane grouping ever meets it. What it catches is `groupBy:
|
|
2478
|
+
* ['combinedId']` on a 44,720-row type — a grouping that is *legal*, produces
|
|
2479
|
+
* one group per row, and is somebody having picked the wrong column.
|
|
2480
|
+
*
|
|
2481
|
+
* The number is a proxy and it is worth saying which part it cannot see: what a
|
|
2482
|
+
* group costs in bytes depends on how many aggregates the node has and how long
|
|
2483
|
+
* a `join` grows. The first is bounded by
|
|
2484
|
+
* {@link WORKFLOW_AGGREGATE_MAX_AGGREGATES}; the second has its own bound on the
|
|
2485
|
+
* aggregate, because it is the one accumulator whose size is not fixed by the
|
|
2486
|
+
* group count.
|
|
2487
|
+
*/
|
|
2488
|
+
export declare const WORKFLOW_AGGREGATE_MAX_GROUPS = 1000000;
|
|
2489
|
+
/**
|
|
2490
|
+
* The highest ceiling an author may ask for.
|
|
2491
|
+
*
|
|
2492
|
+
* Configurable because "how many groups is too many" genuinely depends on the
|
|
2493
|
+
* machine and on how wide the node is, and a hard-coded limit would make the
|
|
2494
|
+
* node unusable for the one legitimate large grouping. Bounded because past this
|
|
2495
|
+
* the answer is not a bigger number — it is that the grouping belongs in the
|
|
2496
|
+
* source query, where the database already has spill-to-disk and this process
|
|
2497
|
+
* does not.
|
|
2498
|
+
*/
|
|
2499
|
+
export declare const WORKFLOW_AGGREGATE_GROUPS_CEILING = 20000000;
|
|
2500
|
+
/**
|
|
2501
|
+
* The default bound on one joined value, in characters.
|
|
2502
|
+
*
|
|
2503
|
+
* 65,535 because that is what a MySQL `TEXT` column holds, and a value the
|
|
2504
|
+
* target column cannot store is the same defect one layer further down. See
|
|
2505
|
+
* `appendJoin` for the full argument, including the five groups per column that
|
|
2506
|
+
* are silently truncated in production today under a limit of 1,024.
|
|
2507
|
+
*/
|
|
2508
|
+
export declare const WORKFLOW_AGGREGATE_JOIN_MAX_LENGTH = 65535;
|
|
2509
|
+
/** The highest an author may raise a `join` bound to. One `MEDIUMTEXT`. */
|
|
2510
|
+
export declare const WORKFLOW_AGGREGATE_JOIN_LENGTH_CEILING = 16777215;
|
|
2511
|
+
/** The longest separator a `join` may use. Long enough for `" | "`, short enough not to be data. */
|
|
2512
|
+
export declare const WORKFLOW_AGGREGATE_MAX_SEPARATOR = 16;
|
|
2513
|
+
/** The separator a `join` uses when the aggregate does not name one. */
|
|
2514
|
+
export declare const WORKFLOW_AGGREGATE_DEFAULT_SEPARATOR = ", ";
|
|
2515
|
+
/**
|
|
2516
|
+
* One computed column: a function, what it reads, and what it is called.
|
|
2517
|
+
*
|
|
2518
|
+
* A list of these rather than a `Record<name, spec>`, which is the opposite of
|
|
2519
|
+
* the shape {@link WorkflowRenameNode.columns} takes, and the reason is that the
|
|
2520
|
+
* two nodes are keyed by different things. A rename is keyed by its **source**
|
|
2521
|
+
* column and a source column can only be renamed once, so an object gets that
|
|
2522
|
+
* refusal for free. An aggregate is keyed by its **output** name, and the same
|
|
2523
|
+
* source column being read by several aggregates — `min(closedDate)` and
|
|
2524
|
+
* `max(closedDate)` — is the normal case rather than the mistake. A list also
|
|
2525
|
+
* keeps the author's order, which is the order the output columns come out in.
|
|
2526
|
+
*
|
|
2527
|
+
* Two aggregates sharing one `as` is the mistake, it is representable, and
|
|
2528
|
+
* {@link aggregateRefusals} names both of them.
|
|
2529
|
+
*/
|
|
2530
|
+
export interface WorkflowAggregate {
|
|
2531
|
+
/**
|
|
2532
|
+
* The output column name. Matches {@link WORKFLOW_FILTER_COLUMN_PATTERN}.
|
|
2533
|
+
*
|
|
2534
|
+
* Constrained for the reason {@link WorkflowRenameNode.columns}' targets are,
|
|
2535
|
+
* and it is the same trap: `property-names.ts` refuses a published property
|
|
2536
|
+
* whose name cannot become a column, a load looks every field up as
|
|
2537
|
+
* `row[name]`, and a name this service cannot carry downstream loads NULL into
|
|
2538
|
+
* every row and reports success.
|
|
2539
|
+
*/
|
|
2540
|
+
as: string;
|
|
2541
|
+
/** Which function. See {@link WORKFLOW_AGGREGATE_FUNCTIONS}. */
|
|
2542
|
+
fn: WorkflowAggregateFunction;
|
|
2543
|
+
/**
|
|
2544
|
+
* The column it reads. Absent is allowed **only** for `count`, where it means
|
|
2545
|
+
* `COUNT(*)` — how many records landed in the group.
|
|
2546
|
+
*
|
|
2547
|
+
* Also matches {@link WORKFLOW_FILTER_COLUMN_PATTERN}, and that is a rule
|
|
2548
|
+
* about *input* rather than output, which the rename node deliberately does
|
|
2549
|
+
* not have. It is here because the filter node already draws the line in the
|
|
2550
|
+
* same place and for the same forward-looking reason: an aggregate that reads
|
|
2551
|
+
* a column no `GROUP BY` could name is one that could never be pushed into the
|
|
2552
|
+
* query the source already runs. A source whose own headers are `Work Order
|
|
2553
|
+
* Id` is what the `rename` node is for, one node upstream.
|
|
2554
|
+
*/
|
|
2555
|
+
column?: string;
|
|
2556
|
+
/**
|
|
2557
|
+
* `join` only. What goes between the values. Absent means
|
|
2558
|
+
* {@link WORKFLOW_AGGREGATE_DEFAULT_SEPARATOR}.
|
|
2559
|
+
*/
|
|
2560
|
+
separator?: string;
|
|
2561
|
+
/**
|
|
2562
|
+
* `join` only. The bound, in characters, that this aggregate refuses at.
|
|
2563
|
+
* Absent means {@link WORKFLOW_AGGREGATE_JOIN_MAX_LENGTH}.
|
|
2564
|
+
*/
|
|
2565
|
+
maxLength?: number;
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Groups records and computes a summary of each group. No author code.
|
|
2569
|
+
*
|
|
2570
|
+
* ## Why this is a node kind, and why it stays small
|
|
2571
|
+
*
|
|
2572
|
+
* The argument the `rename` node made, and it holds unchanged: the generic
|
|
2573
|
+
* {@link WorkflowTransformNode} continues to exist, and **that is what lets this
|
|
2574
|
+
* node stay deliberately narrow forever**. "I need more than this" is answered
|
|
2575
|
+
* with *use a transform*, never with *add a field here*. The refusals in this
|
|
2576
|
+
* docblock are the point of it, not an apology.
|
|
2577
|
+
*
|
|
2578
|
+
* What is different from `rename` is that the payoff is not speed, it is a
|
|
2579
|
+
* **memory bound**, and it is the difference between working and being killed.
|
|
2580
|
+
*
|
|
2581
|
+
* ## The measurement
|
|
2582
|
+
*
|
|
2583
|
+
* flip's `wo` table is a `GROUP BY` over 44,720 SUBWO rows producing 16,119
|
|
2584
|
+
* groups with 49 aggregates. Reproduced as a whole-batch transform — which is
|
|
2585
|
+
* the only way this graph can express it today — it works, and only just:
|
|
2586
|
+
*
|
|
2587
|
+
* - the child process is handed **65.51 MiB** on stdin;
|
|
2588
|
+
* - it answers with a single JSON line of **25.01 MiB, which is 78.2% of the
|
|
2589
|
+
* hard 32 MiB output cap**, buffered by the parent as one string;
|
|
2590
|
+
* - and `readInputs` materialises all 44,720 records before the child starts. A
|
|
2591
|
+
* standalone equivalent peaked at 237 MiB heap and 406 MiB RSS.
|
|
2592
|
+
*
|
|
2593
|
+
* **It fails at 1.28× that file** — one more summed column, or one more month of
|
|
2594
|
+
* work orders — and it fails by being killed at the cap rather than by
|
|
2595
|
+
* degrading. SUBWO is already the largest file in the drop it comes from. So the
|
|
2596
|
+
* commonest aggregation in the system is one column away from dying at a
|
|
2597
|
+
* ceiling.
|
|
2598
|
+
*
|
|
2599
|
+
* A hash aggregate consumes its input as a stream and **holds only the groups**:
|
|
2600
|
+
* 44,720 rows in, 16,119 accumulator rows held, one staged batch decoded at a
|
|
2601
|
+
* time. For most real aggregations the group count is far smaller than the row
|
|
2602
|
+
* count, and that gap is the entire feature.
|
|
2603
|
+
*
|
|
2604
|
+
* ## Which is why the bound is loud rather than absent
|
|
2605
|
+
*
|
|
2606
|
+
* "Holds only the groups" stops being cheap the moment the groups approach the
|
|
2607
|
+
* rows. So {@link maxGroups} is a ceiling that **refuses**, naming the columns
|
|
2608
|
+
* being grouped on. Peaking silently is the failure being fixed, and a node that
|
|
2609
|
+
* fixed it by peaking somewhere else would not have fixed anything.
|
|
2610
|
+
*
|
|
2611
|
+
* ## What comes out
|
|
2612
|
+
*
|
|
2613
|
+
* The group-by columns, then one column per aggregate, in the node's order —
|
|
2614
|
+
* **on every record, always, including where the answer is null**. That makes an
|
|
2615
|
+
* aggregate the only node kind whose output column set is *exact* rather than an
|
|
2616
|
+
* upper bound; see `producedColumns`.
|
|
2617
|
+
*
|
|
2618
|
+
* ## Every edge case, decided rather than discovered
|
|
2619
|
+
*
|
|
2620
|
+
* - **A group-by column not present in a record** — that record groups with the
|
|
2621
|
+
* records whose column is null, and the output carries `null`. SQL groups all
|
|
2622
|
+
* NULLs together, and the stage encoding makes absent-versus-null a difference
|
|
2623
|
+
* in physical layout rather than in meaning, so splitting them would make the
|
|
2624
|
+
* answer depend on which shape a row landed in.
|
|
2625
|
+
* - **A null group key** — a group like any other, exactly as `GROUP BY`
|
|
2626
|
+
* produces.
|
|
2627
|
+
* - **`1` and `"1"` as group keys** — *different* groups. SQL would coerce them
|
|
2628
|
+
* together; merging two things the source considered distinct on a rule nobody
|
|
2629
|
+
* chose is the worse error, and within one load a column is one type. See
|
|
2630
|
+
* `groupKeyOf`.
|
|
2631
|
+
* - **Empty input** — no groups, so no rows staged, which is what `GROUP BY`
|
|
2632
|
+
* over an empty table produces. The sink already refuses to commit an empty
|
|
2633
|
+
* full snapshot, so nothing new is needed and nothing is silently green.
|
|
2634
|
+
* - **An empty {@link groupBy}** — *refused*, and it is worth saying why rather
|
|
2635
|
+
* than leaving it as a bound. With no grouping, SQL returns exactly one row
|
|
2636
|
+
* whether the input had a billion rows or none, and a run that commits one row
|
|
2637
|
+
* of nulls looks identical to a run that summarised everything. A grand total
|
|
2638
|
+
* is genuinely wanted sometimes; the way to ask for it is a constant column
|
|
2639
|
+
* from a transform, which makes the one row visible in the graph.
|
|
2640
|
+
* - **Two aggregates writing the same output name** — refused, naming both, from
|
|
2641
|
+
* the config alone with nothing to run.
|
|
2642
|
+
* - **An aggregate whose `as` collides with a group-by column** — refused, same
|
|
2643
|
+
* sentence, same reason: one name, two values, and every rule for picking a
|
|
2644
|
+
* winner is arbitrary.
|
|
2645
|
+
* - **Aggregating a column that does not exist upstream** — refused at *save*
|
|
2646
|
+
* wherever `workflowKnownColumns` can prove it (below a `rename` that drops
|
|
2647
|
+
* what it does not name, or below a `catalog` source when the caller supplied
|
|
2648
|
+
* a column lookup). Where the graph cannot prove it, the run says so out loud:
|
|
2649
|
+
* a column present in **no** record of the whole run is reported, and `sum`,
|
|
2650
|
+
* `min`, `max` and `join` over nothing answer `null` rather than `0` or `""`,
|
|
2651
|
+
* which is visibly different from a real answer.
|
|
2652
|
+
* - **Mixed types in a `min`/`max`, or non-numeric text in a `sum`** — refused
|
|
2653
|
+
* at run time, naming the column, the group and the values. MySQL coerces;
|
|
2654
|
+
* coercing here would make the answer depend on which record arrived first and
|
|
2655
|
+
* the run would still be green.
|
|
2656
|
+
*
|
|
2657
|
+
* ## Ordering, and what is actually promised
|
|
2658
|
+
*
|
|
2659
|
+
* `GROUP_CONCAT` without an `ORDER BY` is unordered, which is why a comparison
|
|
2660
|
+
* against flip found identical-length, different-order strings. This node joins
|
|
2661
|
+
* in **input order** and emits groups in **first-seen order**, both of which are
|
|
2662
|
+
* functions of the numbered list of staged batches it reads. So two runs over
|
|
2663
|
+
* the same staged input produce the same bytes. What is *not* promised is
|
|
2664
|
+
* stability across a source that returns its own rows in a different order — a
|
|
2665
|
+
* `SELECT` without an `ORDER BY` promises nothing, and this node cannot promise
|
|
2666
|
+
* more than it was handed.
|
|
2667
|
+
*/
|
|
2668
|
+
export interface WorkflowAggregateNode extends WorkflowNodeBase {
|
|
2669
|
+
kind: 'aggregate';
|
|
2670
|
+
/**
|
|
2671
|
+
* The columns that define a group. Never empty; at most
|
|
2672
|
+
* {@link WORKFLOW_AGGREGATE_MAX_GROUP_BY}; each matches
|
|
2673
|
+
* {@link WORKFLOW_FILTER_COLUMN_PATTERN}; no duplicates.
|
|
2674
|
+
*
|
|
2675
|
+
* These are output column names as well as input ones — the group key comes
|
|
2676
|
+
* out under the name it went in under — which is why they carry the target
|
|
2677
|
+
* name rule and not only the input one.
|
|
2678
|
+
*/
|
|
2679
|
+
groupBy: string[];
|
|
2680
|
+
/**
|
|
2681
|
+
* What to compute per group. Never empty; at most
|
|
2682
|
+
* {@link WORKFLOW_AGGREGATE_MAX_AGGREGATES}; no two share an `as`.
|
|
2683
|
+
*
|
|
2684
|
+
* Empty is refused rather than treated as "just deduplicate", which is what it
|
|
2685
|
+
* would silently be: a node emitting the distinct combinations of its group-by
|
|
2686
|
+
* columns and dropping every other column of every row. That is a real
|
|
2687
|
+
* operation and a completely different one, and reaching it by deleting the
|
|
2688
|
+
* last row of a form is how a published type loses forty columns.
|
|
2689
|
+
*/
|
|
2690
|
+
aggregates: WorkflowAggregate[];
|
|
2691
|
+
/**
|
|
2692
|
+
* The ceiling on distinct groups. Absent means
|
|
2693
|
+
* {@link WORKFLOW_AGGREGATE_MAX_GROUPS}; at most
|
|
2694
|
+
* {@link WORKFLOW_AGGREGATE_GROUPS_CEILING}.
|
|
2695
|
+
*/
|
|
2696
|
+
maxGroups?: number;
|
|
2697
|
+
}
|
|
2698
|
+
/** {@link WorkflowAggregateNode.maxGroups}, resolved. One reader of the default. */
|
|
2699
|
+
export declare function workflowAggregateMaxGroups(node: WorkflowAggregateNode): number;
|
|
2700
|
+
/** {@link WorkflowAggregate.separator}, resolved. One reader of the default. */
|
|
2701
|
+
export declare function workflowAggregateSeparator(aggregate: WorkflowAggregate): string;
|
|
2702
|
+
/** {@link WorkflowAggregate.maxLength}, resolved. One reader of the default. */
|
|
2703
|
+
export declare function workflowAggregateJoinMaxLength(aggregate: WorkflowAggregate): number;
|
|
2704
|
+
/** Whether this function reads a column. Only `count` may go without one. */
|
|
2705
|
+
export declare function workflowAggregateNeedsColumn(fn: WorkflowAggregateFunction): boolean;
|
|
2706
|
+
/**
|
|
2707
|
+
* The columns an aggregate node **reads**: its group keys and its inputs.
|
|
2708
|
+
*
|
|
2709
|
+
* What `checkColumnsProduced` tests against what the graph can prove is there,
|
|
2710
|
+
* and what the run log reports as never-seen. Deduplicated and in a stable
|
|
2711
|
+
* order, because it goes into a sentence.
|
|
2712
|
+
*/
|
|
2713
|
+
export declare function workflowAggregateColumns(node: WorkflowAggregateNode): string[];
|
|
2714
|
+
/**
|
|
2715
|
+
* The columns an aggregate node **produces**, which is all of them and nothing
|
|
2716
|
+
* else.
|
|
2717
|
+
*
|
|
2718
|
+
* Closed by the config, and closed *exactly* rather than as an upper bound —
|
|
2719
|
+
* every emitted record carries every one of these keys, whatever was upstream
|
|
2720
|
+
* and whatever the values turned out to be. That is a stronger claim than the
|
|
2721
|
+
* one `rename` introduced, and it is stronger for a structural reason: a rename
|
|
2722
|
+
* only produces a target where the input actually held the source column,
|
|
2723
|
+
* whereas an aggregate writes a group's answer whether or not anything in the
|
|
2724
|
+
* group had a value for it.
|
|
2725
|
+
*
|
|
2726
|
+
* The one thing it does not claim is that the values are useful. An aggregate
|
|
2727
|
+
* over a column that no record carried produces the column, holding `null`.
|
|
2728
|
+
*/
|
|
2729
|
+
export declare function workflowAggregateOutputColumns(node: WorkflowAggregateNode): string[];
|
|
2730
|
+
/**
|
|
2731
|
+
* Every reason an aggregate cannot be stored, as sentences, or empty.
|
|
2732
|
+
*
|
|
2733
|
+
* One function, called by {@link validateWorkflow}, by the HTTP boundary, by the
|
|
2734
|
+
* canvas and by the fold itself, for the reason {@link renameColumnRefusals} is
|
|
2735
|
+
* shared: a screen with its own copy of the identifier pattern is a screen that
|
|
2736
|
+
* accepts something the server refuses, halfway through a save.
|
|
2737
|
+
*
|
|
2738
|
+
* All of them rather than the first, exactly as
|
|
2739
|
+
* {@link refuseUnpublishablePropertyNames} argues: a node with forty aggregates
|
|
2740
|
+
* typed in one sitting is usually wrong about several in the same way.
|
|
2741
|
+
*/
|
|
2742
|
+
export declare function aggregateRefusals(node: {
|
|
2743
|
+
groupBy?: unknown;
|
|
2744
|
+
aggregates?: unknown;
|
|
2745
|
+
maxGroups?: unknown;
|
|
2746
|
+
}): string[];
|
|
2747
|
+
/**
|
|
2748
|
+
* How many fields one lookup may bring across.
|
|
2749
|
+
*
|
|
2750
|
+
* The same argument {@link WORKFLOW_RENAME_MAX_COLUMNS} makes, plus one that is
|
|
2751
|
+
* specific to this node: every named field is held *per distinct key* for the
|
|
2752
|
+
* whole run, so this number multiplies {@link WORKFLOW_LOOKUP_MAX_REFERENCE_ROWS}
|
|
2753
|
+
* into the actual memory bill. Past a few dozen the thing being expressed is
|
|
2754
|
+
* "give me that whole table beside this one", which is a second source and a
|
|
2755
|
+
* union, not an enrichment.
|
|
2756
|
+
*/
|
|
2757
|
+
export declare const WORKFLOW_LOOKUP_MAX_FIELDS = 64;
|
|
2758
|
+
/**
|
|
2759
|
+
* How many rows the reference side may have before the node refuses to run.
|
|
2760
|
+
*
|
|
2761
|
+
* ## Why there is a number here at all
|
|
2762
|
+
*
|
|
2763
|
+
* Because exactly one side of a join can stream, and this node holds the other
|
|
2764
|
+
* one. That is the whole property it exists to have (see
|
|
2765
|
+
* {@link WorkflowLookupNode}) and it is also the whole hazard: a graph whose
|
|
2766
|
+
* reference edge is accidentally wired to the 7.6-million-row side does not fail,
|
|
2767
|
+
* it allocates until the pod is killed — and a pod killed by the kernel produces
|
|
2768
|
+
* no run log, no failed node and no message, which is the silence this file is
|
|
2769
|
+
* arranged against.
|
|
2770
|
+
*
|
|
2771
|
+
* ## Why it is measured in rows, and why it is free
|
|
2772
|
+
*
|
|
2773
|
+
* A staged input announces its `rowCount` before a single row is read back (see
|
|
2774
|
+
* `WorkflowStageRef`), so the refusal happens *before* anything is held. A bound
|
|
2775
|
+
* in bytes would have to be discovered by holding rows until they weighed too
|
|
2776
|
+
* much, which is a bound that has already done the damage by the time it fires.
|
|
2777
|
+
*
|
|
2778
|
+
* Two hundred thousand, and the arithmetic rather than a round number that feels
|
|
2779
|
+
* safe: what is retained per key is the key string plus the values of the named
|
|
2780
|
+
* fields — not the reference row — so a reference of this size with a handful of
|
|
2781
|
+
* short fields is tens of megabytes, and one at {@link WORKFLOW_LOOKUP_MAX_FIELDS}
|
|
2782
|
+
* is the point where it stops being obviously fine. The real reference tables
|
|
2783
|
+
* this was built against are three orders of magnitude below it: a work-plan code
|
|
2784
|
+
* table is hundreds of rows and a unit dictionary is dozens.
|
|
2785
|
+
*
|
|
2786
|
+
* The refusal names the count, the bound and the fix, because the fix is
|
|
2787
|
+
* genuinely available in the graph: put a filter on the reference side, or swap
|
|
2788
|
+
* the two edges if the smaller side is the one being streamed.
|
|
2789
|
+
*/
|
|
2790
|
+
export declare const WORKFLOW_LOOKUP_MAX_REFERENCE_ROWS = 200000;
|
|
2791
|
+
/**
|
|
2792
|
+
* Enriches each row with fields from a reference dataset, matched by key.
|
|
2793
|
+
*
|
|
2794
|
+
* ## The property that justifies a kind rather than a transform
|
|
2795
|
+
*
|
|
2796
|
+
* **One side is held; the other streams.** The reference is read once into a map
|
|
2797
|
+
* keyed by its key column, and then the driving rows go past it one batch at a
|
|
2798
|
+
* time and never accumulate. That asymmetry is the entire content of the node,
|
|
2799
|
+
* and it is not something a {@link WorkflowTransformNode} can express: a
|
|
2800
|
+
* transform is a function over what it is given, so a join written as one has to
|
|
2801
|
+
* be handed *both* sides at once — which is why a transform makes
|
|
2802
|
+
* `ConnectorRunnerService` log *"Held all N records in memory"*, and why the
|
|
2803
|
+
* whole-batch version of this exact join reached 78% of a hard 32 MiB output
|
|
2804
|
+
* bound on 44,720 rows before it did anything interesting.
|
|
2805
|
+
*
|
|
2806
|
+
* A per-record transform ({@link CatalogTransform} in `record` mode) cannot do it
|
|
2807
|
+
* either, and the reason is sharper: a function over one record has nowhere to
|
|
2808
|
+
* put the map. It would rebuild it per record, or reach a database per record,
|
|
2809
|
+
* and 44,720 round trips is not a shape anybody would choose on purpose.
|
|
2810
|
+
*
|
|
2811
|
+
* ## What it stays narrow about, deliberately
|
|
2812
|
+
*
|
|
2813
|
+
* The generic transform still exists, and that is what lets this node refuse
|
|
2814
|
+
* every next field forever — the argument {@link WorkflowRenameNode} makes at
|
|
2815
|
+
* length. No join *type* beyond {@link unmatched}, no composite keys, no
|
|
2816
|
+
* expressions on either side, no aggregation of the matched rows. The answer to
|
|
2817
|
+
* "I need more than this" is always *use a transform*, and never *add a field
|
|
2818
|
+
* here*. Two of those refusals have a second reason on top:
|
|
2819
|
+
*
|
|
2820
|
+
* - **No composite key.** Two columns concatenated is a rule about a separator,
|
|
2821
|
+
* and a separator that occurs inside a value silently merges two different
|
|
2822
|
+
* keys into one. A {@link WorkflowRenameNode} cannot build one either, which is
|
|
2823
|
+
* the honest statement: build the key in a transform, then join on it, and the
|
|
2824
|
+
* separator is a decision somebody wrote down.
|
|
2825
|
+
* - **No expressions on either side.** `UPPER(key)` looks harmless and is the
|
|
2826
|
+
* single most dangerous thing that could be added, because normalising a key
|
|
2827
|
+
* is a rule about which of two values are "the same value" — see the note on
|
|
2828
|
+
* {@link key} about what happens when only one side is normalised.
|
|
2829
|
+
*
|
|
2830
|
+
* ## Where the reference comes from: an edge, named
|
|
2831
|
+
*
|
|
2832
|
+
* The reference is **another node in this graph**, wired into this one, and
|
|
2833
|
+
* {@link reference} says which of the inbound edges it is. Not a connector on
|
|
2834
|
+
* this node, and not the first inbound edge.
|
|
2835
|
+
*
|
|
2836
|
+
* *Not a connector on this node*, because a source is already a modelled thing
|
|
2837
|
+
* with a kind, an optional named connection, a secret, a mode, a config, schema
|
|
2838
|
+
* discovery and a staging path — and putting a second, smaller copy of all that
|
|
2839
|
+
* inside this node would fork it. Wiring a source in instead means the reference
|
|
2840
|
+
* composes with everything: a `catalog` source reads the **current** snapshot of
|
|
2841
|
+
* a published type, resolved when the run reaches it (see `sourceKind: 'catalog'`),
|
|
2842
|
+
* which is the natural reference and names no physical table; a `sql` source
|
|
2843
|
+
* reads a code table straight out of an operational database; and a filter or a
|
|
2844
|
+
* rename may sit in between, which is how a reference with duplicate keys is
|
|
2845
|
+
* made unambiguous (see below) without this node growing a rule for it.
|
|
2846
|
+
*
|
|
2847
|
+
* *Not the first inbound edge*, and this one is the sharp decision. Edge order
|
|
2848
|
+
* is defined and does decide what a multi-input node receives — but for every
|
|
2849
|
+
* other kind, reordering two wires changes only the order rows are concatenated
|
|
2850
|
+
* in, which is at worst cosmetic. Here it would decide **which side is held
|
|
2851
|
+
* entirely in memory**, and swapping them silently turns a working graph into
|
|
2852
|
+
* one that either holds 44,720 rows to enrich 200, or joins the two datasets the
|
|
2853
|
+
* wrong way round and reports success. Reordering edges is invisible on a canvas.
|
|
2854
|
+
* So the node names its reference by node id, and `validateWorkflow` refuses a
|
|
2855
|
+
* name that is not one of its inbound edges.
|
|
2856
|
+
*
|
|
2857
|
+
* ## The counts, which are the point
|
|
2858
|
+
*
|
|
2859
|
+
* The failure this node was built against is not a crash. flip's SUBWO reader
|
|
2860
|
+
* builds exactly these two maps and reads them per row, and when the reference
|
|
2861
|
+
* table is empty it produces **unenriched rows and a green run** — 44,720 rows
|
|
2862
|
+
* with `planName`, `planDescription` and `unitMel` hard null, no error, no
|
|
2863
|
+
* warning, and a documented seeding prerequisite that nothing checks. A zero-match
|
|
2864
|
+
* join is indistinguishable from a working one by looking at the run.
|
|
2865
|
+
*
|
|
2866
|
+
* So the run log always carries three numbers, whatever {@link unmatched} says:
|
|
2867
|
+
* how many rows matched, how many had a key that matched nothing, and how many
|
|
2868
|
+
* had no key at all. The third is separate from the second on purpose — they
|
|
2869
|
+
* have different causes and different fixes, and flip's reader folds both into
|
|
2870
|
+
* the same NULL. A run where nothing matched gets a line of its own, the way
|
|
2871
|
+
* `filterLogLines` calls out a filter that kept nothing.
|
|
2872
|
+
*
|
|
2873
|
+
* ## The decisions, each made rather than discovered
|
|
2874
|
+
*
|
|
2875
|
+
* - **A key that matches nothing** — {@link unmatched}, defaulting to `null`.
|
|
2876
|
+
* - **Two reference rows for one key** — refused, *when they disagree*. See
|
|
2877
|
+
* {@link fields}.
|
|
2878
|
+
* - **An enriched name the driving row already carries** — filled when it is
|
|
2879
|
+
* empty, refused when it holds a value. The rule is about destroying data
|
|
2880
|
+
* rather than about a name being taken, and the distinction is the whole
|
|
2881
|
+
* usefulness of the node: a published type *declares* the columns it holds, so
|
|
2882
|
+
* a graph reading one back to enrich it receives every one of them. The real
|
|
2883
|
+
* measurement is the argument — `SubwoReplica` hands over 44,720 rows all
|
|
2884
|
+
* carrying `planName`, `planDescription` and `unitMel` with `null` in them,
|
|
2885
|
+
* which is exactly the three columns this node was built to fill. A target
|
|
2886
|
+
* holding an actual value is two columns and one name and fails the node,
|
|
2887
|
+
* naming the row, which is the sentence {@link renameColumnRefusals} says about
|
|
2888
|
+
* the same problem arriving from the other direction.
|
|
2889
|
+
* - **A reference row with no key** — not indexed, and counted. A real work-plan
|
|
2890
|
+
* table has them: flip writes `planId: row.planId ?? ""` when a load has no
|
|
2891
|
+
* plan code, so the empty-string key is in the table by construction and can
|
|
2892
|
+
* never be matched by anything. Refusing the whole reference over one of those
|
|
2893
|
+
* would make the node unusable against the data it was built for; indexing it
|
|
2894
|
+
* silently would let one keyless row become the answer for every keyless
|
|
2895
|
+
* driving row.
|
|
2896
|
+
*/
|
|
2897
|
+
export interface WorkflowLookupNode extends WorkflowNodeBase {
|
|
2898
|
+
kind: 'lookup';
|
|
2899
|
+
/**
|
|
2900
|
+
* The id of the inbound node whose rows are the reference side.
|
|
2901
|
+
*
|
|
2902
|
+
* Held in memory for the whole node; everything else wired in streams past it.
|
|
2903
|
+
* `validateWorkflow` refuses an id that is not one of this node's inbound
|
|
2904
|
+
* edges, and refuses a lookup whose *only* inbound edge is this one — a lookup
|
|
2905
|
+
* with nothing to enrich produces nothing, and would commit an empty snapshot.
|
|
2906
|
+
*/
|
|
2907
|
+
reference: string;
|
|
2908
|
+
/**
|
|
2909
|
+
* The column on the **driving** row holding the key.
|
|
2910
|
+
*
|
|
2911
|
+
* Deliberately unconstrained in spelling, the way a rename's *source* names
|
|
2912
|
+
* are: `Reg Number` and `Mgmt Cd` are what real drops are keyed by.
|
|
2913
|
+
*
|
|
2914
|
+
* ## How two keys are compared, stated once because it is the whole join
|
|
2915
|
+
*
|
|
2916
|
+
* A key is read off the row, and a value of `null` or `undefined` — or a
|
|
2917
|
+
* column that is absent from that row — means the row **has no key**. It is
|
|
2918
|
+
* counted separately and never matches, including against a reference row
|
|
2919
|
+
* that also has no key.
|
|
2920
|
+
*
|
|
2921
|
+
* Anything else is compared **as a string**, by `String(value)`, with no
|
|
2922
|
+
* trimming, no case folding and no other normalisation.
|
|
2923
|
+
*
|
|
2924
|
+
* The coercion is a decision and so is its limit. It is there because the two
|
|
2925
|
+
* sides routinely come from different engines: a work-plan code arriving as a
|
|
2926
|
+
* MySQL `VARCHAR` and the same code arriving as a number out of a spreadsheet
|
|
2927
|
+
* parser are the same key to everyone except `===`, and a join that matched
|
|
2928
|
+
* nothing for that reason is the exact silent-zero this node reports counts to
|
|
2929
|
+
* prevent. What is *not* done is normalising the shape of the value, because
|
|
2930
|
+
* every one of those is a rule about which of somebody's values are the same
|
|
2931
|
+
* value: `"21 CES"` and `"21CES"` are not the same unit unless a person says
|
|
2932
|
+
* so, and flip's own reader is the cautionary tale — it normalises the driving
|
|
2933
|
+
* unit and compares it against a reference column normalised at write time by
|
|
2934
|
+
* a different screen, so the two agree only for as long as nobody edits either.
|
|
2935
|
+
* If a key needs normalising, normalise it in a transform, on both sides,
|
|
2936
|
+
* where it is visible.
|
|
2937
|
+
*/
|
|
2938
|
+
key: string;
|
|
2939
|
+
/** The column on the **reference** row holding the key. Compared as {@link key} describes. */
|
|
2940
|
+
referenceKey: string;
|
|
2941
|
+
/**
|
|
2942
|
+
* Reference column → the name it lands under on the driving row. Never empty;
|
|
2943
|
+
* at most {@link WORKFLOW_LOOKUP_MAX_FIELDS} entries; every target matches
|
|
2944
|
+
* {@link WORKFLOW_FILTER_COLUMN_PATTERN} and no two share one.
|
|
2945
|
+
*
|
|
2946
|
+
* A `Record` rather than a list of pairs, for the reason
|
|
2947
|
+
* {@link WorkflowRenameNode.columns} is one: a key cannot appear twice, so
|
|
2948
|
+
* bringing one reference column across twice is unrepresentable. Two brought
|
|
2949
|
+
* across *onto* one name is representable and refused.
|
|
2950
|
+
*
|
|
2951
|
+
* The target pattern is the identifier rule and it is here for the reason the
|
|
2952
|
+
* rename's targets are: a load looks every field up as `row[name]`, so a name
|
|
2953
|
+
* no property can carry loads NULL into every row and reports success. That is
|
|
2954
|
+
* the failure this node is supposed to be *fixing*, so producing it would be
|
|
2955
|
+
* the trap re-armed one node further along.
|
|
2956
|
+
*
|
|
2957
|
+
* ## Empty is refused rather than treated as a no-op
|
|
2958
|
+
*
|
|
2959
|
+
* A lookup that brings nothing across is a node that draws as configured,
|
|
2960
|
+
* costs a full pass over both sides, and changes nothing — except under `drop`,
|
|
2961
|
+
* where it silently becomes a semi-join that deletes every row whose key is not
|
|
2962
|
+
* in the reference. Two silent opposites reached by deleting the last row of a
|
|
2963
|
+
* form, which is exactly the argument {@link renameColumnRefusals} makes.
|
|
2964
|
+
*
|
|
2965
|
+
* ## Why the fields decide the duplicate-key rule
|
|
2966
|
+
*
|
|
2967
|
+
* Two reference rows for one key means either the join multiplies rows or
|
|
2968
|
+
* something picks a winner, and picking a winner is a rule about whose data
|
|
2969
|
+
* survives — the reasoning {@link renameColumnRefusals} already refused, for
|
|
2970
|
+
* two columns renamed onto one name. Multiplying rows is worse here than it
|
|
2971
|
+
* looks: this node's contract is that it enriches, so a sink downstream would
|
|
2972
|
+
* commit more rows than were read with nothing on the canvas saying why.
|
|
2973
|
+
*
|
|
2974
|
+
* So: **two reference rows for one key are refused when they disagree about
|
|
2975
|
+
* any named field, and collapsed when they agree.** Agreeing costs nobody
|
|
2976
|
+
* anything — there is no winner, the answer is the same either way — and it is
|
|
2977
|
+
* what a real reference table looks like when it has one row per key *and*
|
|
2978
|
+
* something else, which is the common case. Disagreeing fails the node, naming
|
|
2979
|
+
* the key, the field and both values.
|
|
2980
|
+
*
|
|
2981
|
+
* Note what makes this rule cheap and total: it is decided over the **named
|
|
2982
|
+
* fields only**, so two reference rows that differ in a column this node does
|
|
2983
|
+
* not bring across are not a conflict, because nothing about them reaches the
|
|
2984
|
+
* output. And it is decided while the map is built, before a single driving
|
|
2985
|
+
* row is read, so it fails at the start of the node rather than at row ninety
|
|
2986
|
+
* thousand.
|
|
2987
|
+
*
|
|
2988
|
+
* flip's own reader is the argument for refusing rather than choosing. It
|
|
2989
|
+
* builds the plan map with `plansMap.set(plan.planId, plan)`, which keeps the
|
|
2990
|
+
* **last** row; it resolves the unit dictionary with `Array.prototype.find`,
|
|
2991
|
+
* which keeps the **first**; neither key column has a unique constraint; and
|
|
2992
|
+
* the two rules live forty lines apart in one file. Nobody chose either of
|
|
2993
|
+
* them.
|
|
2994
|
+
*/
|
|
2995
|
+
fields: Record<string, string>;
|
|
2996
|
+
/**
|
|
2997
|
+
* What happens to a driving row whose key matches nothing. Absent means `null`.
|
|
2998
|
+
*
|
|
2999
|
+
* `null` is the default because it is the only one of the three that changes
|
|
3000
|
+
* neither which rows exist nor whether the run finishes, so it is the one that
|
|
3001
|
+
* can be the answer for a graph nobody has thought about yet. It is also what
|
|
3002
|
+
* flip does today — and the difference this node makes is not the disposition,
|
|
3003
|
+
* it is that the count is reported rather than left to be discovered by
|
|
3004
|
+
* querying the committed snapshot.
|
|
3005
|
+
*
|
|
3006
|
+
* `drop` is an INNER JOIN and it removes rows, so it is never a default: a
|
|
3007
|
+
* lookup wired in front of a full-mode sink under `drop` shrinks a published
|
|
3008
|
+
* type, which is the accident {@link WorkflowFilterNode.narrows} exists about.
|
|
3009
|
+
*
|
|
3010
|
+
* `fail` is for a reference that is a **documented prerequisite**, which is not
|
|
3011
|
+
* a hypothetical: flip's docs make seeding the unit dictionary a prerequisite
|
|
3012
|
+
* of MEL, MVR and SUBWO, and an unseeded one yields unnormalized rows rather
|
|
3013
|
+
* than an error. A load that cannot be enriched is a load that should not
|
|
3014
|
+
* commit, and this is how somebody says so.
|
|
3015
|
+
*/
|
|
3016
|
+
unmatched?: WorkflowLookupUnmatched;
|
|
3017
|
+
}
|
|
3018
|
+
/** {@link WorkflowLookupNode.unmatched}, resolved. One reader of the default. */
|
|
3019
|
+
export declare function workflowLookupUnmatched(node: WorkflowLookupNode): WorkflowLookupUnmatched;
|
|
3020
|
+
/**
|
|
3021
|
+
* A key, as this node compares them, or `undefined` for a row that has none.
|
|
3022
|
+
*
|
|
3023
|
+
* One function, exported, and called by the runner for both sides — because the
|
|
3024
|
+
* one way a join goes silently wrong is the two sides being read by two pieces
|
|
3025
|
+
* of code that agree today. See {@link WorkflowLookupNode.key} for what it does
|
|
3026
|
+
* and, more to the point, what it deliberately does not do.
|
|
3027
|
+
*/
|
|
3028
|
+
export declare function workflowLookupKey(value: unknown): string | undefined;
|
|
3029
|
+
/**
|
|
3030
|
+
* Every reason a lookup's configuration cannot be stored, as sentences, or empty.
|
|
3031
|
+
*
|
|
3032
|
+
* One function, called by {@link validateWorkflow}, by the HTTP boundary and by
|
|
3033
|
+
* the canvas, for the reason {@link renameColumnRefusals} is: a screen that
|
|
3034
|
+
* checked a target name against its own copy of the pattern is a screen that
|
|
3035
|
+
* eventually accepts something the server refuses, halfway through a save.
|
|
3036
|
+
*
|
|
3037
|
+
* All of them rather than the first, for the reason `refuseUnpublishablePropertyNames`
|
|
3038
|
+
* gives: a form filled in one sitting is usually wrong about several things in
|
|
3039
|
+
* the same way.
|
|
3040
|
+
*
|
|
3041
|
+
* The *wiring* rules — that {@link WorkflowLookupNode.reference} names an inbound
|
|
3042
|
+
* edge, and that something other than the reference is wired in — are not here,
|
|
3043
|
+
* and that is not an omission. They are facts about the graph rather than about
|
|
3044
|
+
* the node, so they cannot be answered from the node alone; `validateWorkflow`
|
|
3045
|
+
* owns them and the inspector reads them from `validateWorkflow`.
|
|
3046
|
+
*/
|
|
3047
|
+
export declare function lookupConfigRefusals(node: {
|
|
3048
|
+
key?: unknown;
|
|
3049
|
+
referenceKey?: unknown;
|
|
3050
|
+
fields?: Record<string, string>;
|
|
3051
|
+
}): string[];
|
|
3052
|
+
/**
|
|
3053
|
+
* Whether a stored aggregate list is one this build can run.
|
|
3054
|
+
*
|
|
3055
|
+
* Refused rather than repaired, the stance {@link isWorkflowRenameColumns} takes
|
|
3056
|
+
* and for the same reason one step further along: an aggregate list read back
|
|
3057
|
+
* with one entry silently dropped is a graph that commits a column of nulls
|
|
3058
|
+
* under a name somebody put in an object type on purpose.
|
|
3059
|
+
*/
|
|
3060
|
+
export declare function isWorkflowAggregates(value: unknown): value is WorkflowAggregate[];
|
|
3061
|
+
/**
|
|
3062
|
+
* Whether a stored field map is one this build can run.
|
|
3063
|
+
*
|
|
3064
|
+
* Refused rather than repaired, the stance {@link isWorkflowRenameColumns} takes
|
|
3065
|
+
* and for the same reason: a map read back with one entry silently dropped is a
|
|
3066
|
+
* graph that commits a column of NULLs under a name nobody can now explain.
|
|
3067
|
+
*
|
|
3068
|
+
* `Object.entries` rather than a `for…in`, so an inherited key cannot enter the
|
|
3069
|
+
* map. The key columns are not checked here because they are not this value;
|
|
3070
|
+
* {@link lookupConfigRefusals} is what sees the whole node.
|
|
3071
|
+
*/
|
|
3072
|
+
export declare function isWorkflowLookupFields(value: unknown): value is Record<string, string>;
|
|
2343
3073
|
/**
|
|
2344
3074
|
* A discriminated union, so narrowing a node is `node.kind === "sink"` and
|
|
2345
3075
|
* never a type assertion. This is why the kind list is not simply a string on
|
|
2346
3076
|
* one node shape with every field optional: that shape lets a source node carry
|
|
2347
3077
|
* a `transformId` and nothing catches it.
|
|
2348
3078
|
*/
|
|
2349
|
-
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode | WorkflowRenameNode;
|
|
3079
|
+
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode | WorkflowRenameNode | WorkflowAggregateNode | WorkflowLookupNode;
|
|
2350
3080
|
/**
|
|
2351
3081
|
* The node kinds that can be saved once and used in several graphs.
|
|
2352
3082
|
*
|
|
@@ -2403,6 +3133,18 @@ export declare function isReusableNodeKind(value: unknown): value is ReusableNod
|
|
|
2403
3133
|
* *about* one drop of one file. `Mgmt Cd → mgmtCd` saved under a name and
|
|
2404
3134
|
* dropped into a graph reading a different system renames nothing at all, and
|
|
2405
3135
|
* the symptom is a column of NULLs rather than a failure.
|
|
3136
|
+
* - `aggregate` — the same again, and it fails in both directions at once. Its
|
|
3137
|
+
* group-by columns and its inputs name one type's columns, so a shared one
|
|
3138
|
+
* groups a graph it was not written for on a column that is not there — which
|
|
3139
|
+
* collapses every record into one null-keyed group rather than erroring. And
|
|
3140
|
+
* its *output* column set is the thing downstream nodes are validated against,
|
|
3141
|
+
* so a shared node editable from elsewhere would silently change what another
|
|
3142
|
+
* graph's sink is allowed to write.
|
|
3143
|
+
* - `lookup` — that argument, and one that is not an argument at all but an
|
|
3144
|
+
* impossibility: {@link WorkflowLookupNode.reference} is **a node id in this
|
|
3145
|
+
* graph**. A shared body carrying one would name a node the adopting graph has
|
|
3146
|
+
* never had, and it is not a cosmetic field — it is the one that decides which
|
|
3147
|
+
* side of the join is held in memory.
|
|
2406
3148
|
*/
|
|
2407
3149
|
export declare const NODE_KIND_IS_REUSABLE: {
|
|
2408
3150
|
readonly source: true;
|
|
@@ -2412,6 +3154,8 @@ export declare const NODE_KIND_IS_REUSABLE: {
|
|
|
2412
3154
|
readonly if: false;
|
|
2413
3155
|
readonly filter: false;
|
|
2414
3156
|
readonly rename: false;
|
|
3157
|
+
readonly aggregate: false;
|
|
3158
|
+
readonly lookup: false;
|
|
2415
3159
|
};
|
|
2416
3160
|
/** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
|
|
2417
3161
|
export declare function nodeKindIsReusable(kind: WorkflowNodeKind): boolean;
|
|
@@ -3210,7 +3954,7 @@ export interface CallableWorkflowBlock {
|
|
|
3210
3954
|
}
|
|
3211
3955
|
export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
|
|
3212
3956
|
/** Every way a graph can be refused. Exported so a canvas can key off the code. */
|
|
3213
|
-
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", "source-type-not-named", "call-not-named", "call-plain-has-output", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing", "rename-invalid", "column-not-produced", "version-pin-invalid"];
|
|
3957
|
+
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", "source-type-not-named", "call-not-named", "call-plain-has-output", "if-not-named", "if-threshold-invalid", "if-needs-one-input", "branch-not-labelled", "branch-on-plain-edge", "filter-predicate-invalid", "filter-narrows-unacknowledged", "filter-narrows-nothing", "rename-invalid", "aggregate-invalid", "lookup-invalid", "lookup-reference-not-wired", "lookup-nothing-to-enrich", "column-not-produced", "version-pin-invalid"];
|
|
3214
3958
|
export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
|
|
3215
3959
|
export interface WorkflowValidationIssue {
|
|
3216
3960
|
code: WorkflowIssueCode;
|
|
@@ -3443,7 +4187,39 @@ export declare function workflowFilterColumns(predicate: WorkflowFilterPredicate
|
|
|
3443
4187
|
* cyclic graph before it gets here, but the canvas calls this while a graph is
|
|
3444
4188
|
* being drawn and is entitled to a wrong-but-terminating answer.
|
|
3445
4189
|
*/
|
|
3446
|
-
export declare function workflowKnownColumns(graph: WorkflowGraph, nodeId: string, knowledge?: WorkflowColumnKnowledge): ReadonlySet<string> | undefined;
|
|
4190
|
+
export declare function workflowKnownColumns(graph: WorkflowGraph, nodeId: string, knowledge?: WorkflowColumnKnowledge, onlyFrom?: WorkflowInputFilter): ReadonlySet<string> | undefined;
|
|
4191
|
+
/**
|
|
4192
|
+
* Which of a node's inbound edges a question is about.
|
|
4193
|
+
*
|
|
4194
|
+
* One node kind needs this and it is {@link WorkflowLookupNode}, which is the
|
|
4195
|
+
* only one whose inputs are not interchangeable: the reference side is held as a
|
|
4196
|
+
* map and its columns do **not** flow on, so the union of everything wired in is
|
|
4197
|
+
* the wrong answer to "what does this node pass down". Without the distinction
|
|
4198
|
+
* the walk would report the reference's columns as available downstream, and a
|
|
4199
|
+
* filter naming one of them would be accepted by the validator and then match no
|
|
4200
|
+
* row at run time — the precise silent failure `checkColumnsProduced` exists to
|
|
4201
|
+
* catch, produced by the check itself.
|
|
4202
|
+
*
|
|
4203
|
+
* Optional everywhere it appears, and omitting it means every inbound edge —
|
|
4204
|
+
* which is what every other kind wants and what every existing caller gets.
|
|
4205
|
+
*/
|
|
4206
|
+
export type WorkflowInputFilter = (fromNodeId: string) => boolean;
|
|
4207
|
+
/**
|
|
4208
|
+
* The two column sets a lookup sees, told apart.
|
|
4209
|
+
*
|
|
4210
|
+
* Exported because three callers need the same split and each one getting it
|
|
4211
|
+
* right separately is how they come to disagree: the validator refuses a key
|
|
4212
|
+
* column that is not on the driving side, the walk answers what the node passes
|
|
4213
|
+
* on, and the inspector says both out loud on the screen where the columns are
|
|
4214
|
+
* typed.
|
|
4215
|
+
*
|
|
4216
|
+
* Either side answers `undefined` for the ordinary reason — see
|
|
4217
|
+
* {@link workflowKnownColumns} — and `undefined` must not be read as empty.
|
|
4218
|
+
*/
|
|
4219
|
+
export declare function workflowLookupColumns(graph: WorkflowGraph, node: WorkflowLookupNode, knowledge?: WorkflowColumnKnowledge): {
|
|
4220
|
+
driving: ReadonlySet<string> | undefined;
|
|
4221
|
+
reference: ReadonlySet<string> | undefined;
|
|
4222
|
+
};
|
|
3447
4223
|
/**
|
|
3448
4224
|
* What a caller can tell the column walk that the graph does not hold.
|
|
3449
4225
|
*
|