@dudousxd/nestjs-catalog 0.25.0 → 0.27.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 +471 -9
- package/dist/catalog.pipeline.js +760 -33
- package/dist/catalog.store.d.ts +61 -0
- package/dist/catalog.store.js +7 -0
- package/dist/client.d.ts +4 -4
- package/dist/client.js +42 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +41 -4
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import { type CatalogRevision } from './catalog.workspace';
|
|
|
15
15
|
* execute — a kind that exists in the type and throws at run time is worse than
|
|
16
16
|
* one that is absent, because the first looks supported in a dropdown.
|
|
17
17
|
*/
|
|
18
|
-
export declare const CONNECTOR_KINDS: readonly ["http", "sql", "file", "s3", "inline"];
|
|
18
|
+
export declare const CONNECTOR_KINDS: readonly ["http", "sql", "file", "s3", "inline", "catalog"];
|
|
19
19
|
export type ConnectorKind = (typeof CONNECTOR_KINDS)[number];
|
|
20
20
|
/**
|
|
21
21
|
* The type is derived from the list, not written beside it.
|
|
@@ -27,6 +27,42 @@ export type ConnectorKind = (typeof CONNECTOR_KINDS)[number];
|
|
|
27
27
|
* entirely. Anything narrowing a stored value narrows against *this*.
|
|
28
28
|
*/
|
|
29
29
|
export declare function isConnectorKind(value: unknown): value is ConnectorKind;
|
|
30
|
+
/**
|
|
31
|
+
* The connector kind that never compiles quietly.
|
|
32
|
+
*
|
|
33
|
+
* The {@link unreachableNodeKind} of source kinds, and it arrived with
|
|
34
|
+
* `'catalog'` for the reason that kind arrived: two of the places that decide
|
|
35
|
+
* something *per source kind* — the fetcher map and the column question below —
|
|
36
|
+
* were keyed by `string` or answered for `source` as a whole, so a sixth kind
|
|
37
|
+
* could be added and be picked up by neither. A palette that offers a kind
|
|
38
|
+
* nothing can read is the failure {@link CONNECTOR_KINDS} opens by describing,
|
|
39
|
+
* one level down.
|
|
40
|
+
*
|
|
41
|
+
* It throws as well as failing to compile, for the reason its sibling does: a
|
|
42
|
+
* connector's `kind` is a string in a database row, so one written by a newer
|
|
43
|
+
* deployment and read by an older one is possible, and returning a default for
|
|
44
|
+
* it would read the wrong system entirely.
|
|
45
|
+
*/
|
|
46
|
+
export declare function unreachableConnectorKind(kind: never, where: string): never;
|
|
47
|
+
/**
|
|
48
|
+
* The config key a `catalog` source names its object type in.
|
|
49
|
+
*
|
|
50
|
+
* A constant rather than a literal in four files, because it is the one field
|
|
51
|
+
* that kind has and it is written by a form, read by a fetcher, checked by a
|
|
52
|
+
* validator and hashed into the graph's fingerprint.
|
|
53
|
+
*/
|
|
54
|
+
export declare const CATALOG_SOURCE_TYPE_KEY = "objectType";
|
|
55
|
+
/**
|
|
56
|
+
* Which object type a source reads, when it is a `catalog` source that names one.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` covers both "not that kind" and "that kind, unconfigured", and the
|
|
59
|
+
* two callers want the same thing from both: a source that does not name a type
|
|
60
|
+
* is not one whose columns are known, and it is one the validator refuses. The
|
|
61
|
+
* string is trimmed, because a name that differs from a published type only by
|
|
62
|
+
* surrounding whitespace is a load that resolves nothing at run time and a
|
|
63
|
+
* refusal nobody can see the cause of.
|
|
64
|
+
*/
|
|
65
|
+
export declare function workflowSourceObjectType(node: WorkflowNode | Record<string, unknown>): string | undefined;
|
|
30
66
|
/**
|
|
31
67
|
* How the bytes behind a `file` or `s3` connector are read as records.
|
|
32
68
|
*
|
|
@@ -1163,7 +1199,7 @@ export interface WorkflowNodeOutcome {
|
|
|
1163
1199
|
* outside a run, which is why `call` names one and not a step. If a step is
|
|
1164
1200
|
* what you want, the thing to call is a one-step workflow wrapping it.
|
|
1165
1201
|
*/
|
|
1166
|
-
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter", "rename"];
|
|
1202
|
+
export declare const WORKFLOW_NODE_KINDS: readonly ["source", "transform", "sink", "call", "if", "filter", "rename", "aggregate"];
|
|
1167
1203
|
export type WorkflowNodeKind = (typeof WORKFLOW_NODE_KINDS)[number];
|
|
1168
1204
|
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
1169
1205
|
export declare function isWorkflowNodeKind(value: unknown): value is WorkflowNodeKind;
|
|
@@ -2304,13 +2340,392 @@ export declare function renameColumnRefusals(columns: Record<string, string>): s
|
|
|
2304
2340
|
* map, and the values are checked one by one rather than trusted from the type.
|
|
2305
2341
|
*/
|
|
2306
2342
|
export declare function isWorkflowRenameColumns(value: unknown): value is Record<string, string>;
|
|
2343
|
+
/**
|
|
2344
|
+
* The aggregate functions this node computes, and the rule that closes the list.
|
|
2345
|
+
*
|
|
2346
|
+
* A closed list with an exhaustiveness guard, for the reason every other list in
|
|
2347
|
+
* this file is one. What is different here is that the list has a **stated
|
|
2348
|
+
* admission rule**, because "we will keep it narrow" is a promise nobody can
|
|
2349
|
+
* check and a rule is:
|
|
2350
|
+
*
|
|
2351
|
+
* > A function is in if it can be computed from a **fixed-size accumulator**,
|
|
2352
|
+
* > and if its answer does not depend on a decision the config would have to
|
|
2353
|
+
* > carry.
|
|
2354
|
+
*
|
|
2355
|
+
* The first half is the node's whole reason to exist. A hash aggregate is cheap
|
|
2356
|
+
* because it holds one entry per group; an accumulator whose size grows with the
|
|
2357
|
+
* number of *rows* in a group puts the rows back in memory and gives up the
|
|
2358
|
+
* property. The second half is what keeps the config from becoming a small
|
|
2359
|
+
* language: a function that needs an extra field to say what it means is a
|
|
2360
|
+
* function whose meaning was not decided.
|
|
2361
|
+
*
|
|
2362
|
+
* What the rule excludes, so the omissions are on the record rather than
|
|
2363
|
+
* implied:
|
|
2364
|
+
*
|
|
2365
|
+
* - **`countDistinct`** — the sharpest one. It needs a set of the distinct
|
|
2366
|
+
* values *per group per column*, so its accumulator is O(distinct values) and
|
|
2367
|
+
* a high-cardinality column inside a group holds the load. It is the exact
|
|
2368
|
+
* thing this node was built to stop doing, wearing an aggregate's name. A
|
|
2369
|
+
* sketch (HyperLogLog) is fixed-size and is a different function — an
|
|
2370
|
+
* estimate — which is not something to ship under the word `distinct`.
|
|
2371
|
+
* - **`median`, percentiles, `stddev` of a stream** — all need the values, or a
|
|
2372
|
+
* digest that is an approximation with an error bound the config would have to
|
|
2373
|
+
* carry.
|
|
2374
|
+
* - **`first` / `last`** — fixed-size, and excluded on the other half of the
|
|
2375
|
+
* rule: they mean "in input order", and this node's input order is a
|
|
2376
|
+
* `SELECT` without an `ORDER BY`. An aggregate that returns a different value
|
|
2377
|
+
* on a rerun is a load nobody can diff. `min`/`max` are the order-independent
|
|
2378
|
+
* version and are what somebody reaching for `first` usually wants.
|
|
2379
|
+
* - **Conditional aggregation — `MAX(CASE WHEN … THEN … END)`** — deliberately
|
|
2380
|
+
* out of scope, and it is the one omission a reader of flip's `wo` query will
|
|
2381
|
+
* go looking for, because that query has a three-branch status ladder in it.
|
|
2382
|
+
* Admitting it means admitting a predicate *inside* an aggregate, which is a
|
|
2383
|
+
* second expression language nested in the first, evaluated per row per
|
|
2384
|
+
* aggregate. That is transform territory and the generic
|
|
2385
|
+
* {@link WorkflowTransformNode} still exists. What the ladder actually is, is
|
|
2386
|
+
* a priority ordering over a closed set of codes, and it composes: map the
|
|
2387
|
+
* code to a rank in a transform above this node, `min` the rank, map it back
|
|
2388
|
+
* below. Two cheap per-record steps instead of a language.
|
|
2389
|
+
* - **`avg` is in**, and it is in *because* of the rule rather than despite it.
|
|
2390
|
+
* It is `sum` and `count` in one accumulator, both of which are already here,
|
|
2391
|
+
* and SQL has exactly one answer for it. Excluding it would have made the list
|
|
2392
|
+
* an arbitrary set that happened to cover one query, which is the thing the
|
|
2393
|
+
* rule is for.
|
|
2394
|
+
*/
|
|
2395
|
+
export declare const WORKFLOW_AGGREGATE_FUNCTIONS: readonly ["count", "sum", "avg", "min", "max", "join"];
|
|
2396
|
+
export type WorkflowAggregateFunction = (typeof WORKFLOW_AGGREGATE_FUNCTIONS)[number];
|
|
2397
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
2398
|
+
export declare function isWorkflowAggregateFunction(value: unknown): value is WorkflowAggregateFunction;
|
|
2399
|
+
/**
|
|
2400
|
+
* {@link unreachableNodeKind}, one level down, and for the identical reason.
|
|
2401
|
+
*
|
|
2402
|
+
* Every branch over {@link WorkflowAggregateFunction} ends here, so a seventh
|
|
2403
|
+
* function added to the list without an accumulator, a finisher, a canonical
|
|
2404
|
+
* form and a sentence is a type error naming the file rather than a node that
|
|
2405
|
+
* saves, draws and then computes nothing. It throws as well, because a function
|
|
2406
|
+
* name arrives as JSON out of a column and a build older than the data is a
|
|
2407
|
+
* thing that happens.
|
|
2408
|
+
*/
|
|
2409
|
+
export declare function unreachableAggregateFunction(fn: never, where: string): never;
|
|
2410
|
+
/**
|
|
2411
|
+
* How many columns one node may group on.
|
|
2412
|
+
*
|
|
2413
|
+
* The same argument {@link WORKFLOW_RENAME_MAX_COLUMNS} makes, plus one specific
|
|
2414
|
+
* to this node: every extra group-by column can only ever *increase* the number
|
|
2415
|
+
* of groups, so a long list is the shape a high-cardinality grouping arrives in.
|
|
2416
|
+
* flip's real derivation groups on two.
|
|
2417
|
+
*/
|
|
2418
|
+
export declare const WORKFLOW_AGGREGATE_MAX_GROUP_BY = 16;
|
|
2419
|
+
/**
|
|
2420
|
+
* How many aggregates one node may compute.
|
|
2421
|
+
*
|
|
2422
|
+
* flip's `wo` derivation has 49, so the bound has to be comfortably above that
|
|
2423
|
+
* or the node does not do the job it was written for. Past a few hundred the
|
|
2424
|
+
* thing being expressed is a table definition rather than a summary, and the
|
|
2425
|
+
* cost is real: every aggregate is an accumulator held **per group**, so this
|
|
2426
|
+
* number multiplies {@link WORKFLOW_AGGREGATE_MAX_GROUPS} in the heap.
|
|
2427
|
+
*/
|
|
2428
|
+
export declare const WORKFLOW_AGGREGATE_MAX_AGGREGATES = 256;
|
|
2429
|
+
/**
|
|
2430
|
+
* The default ceiling on distinct groups, and the loud refusal that goes with
|
|
2431
|
+
* it.
|
|
2432
|
+
*
|
|
2433
|
+
* A hash aggregate is cheap **only while the groups are far fewer than the
|
|
2434
|
+
* rows**. Group on a near-unique column and it holds one accumulator row per
|
|
2435
|
+
* input row, which is the whole-batch behaviour this node replaces, arrived at
|
|
2436
|
+
* by a different route and with nothing on the canvas to point at. So the
|
|
2437
|
+
* ceiling exists, it is crossed loudly, and the message names the columns being
|
|
2438
|
+
* grouped on — because a bound that is merely reported is a bound that is
|
|
2439
|
+
* discovered by the machine running out of memory.
|
|
2440
|
+
*
|
|
2441
|
+
* A million is chosen against the measurement rather than as a round number:
|
|
2442
|
+
* flip's derivation holds 16,119, so the default is 62× the real case and no
|
|
2443
|
+
* author of a sane grouping ever meets it. What it catches is `groupBy:
|
|
2444
|
+
* ['combinedId']` on a 44,720-row type — a grouping that is *legal*, produces
|
|
2445
|
+
* one group per row, and is somebody having picked the wrong column.
|
|
2446
|
+
*
|
|
2447
|
+
* The number is a proxy and it is worth saying which part it cannot see: what a
|
|
2448
|
+
* group costs in bytes depends on how many aggregates the node has and how long
|
|
2449
|
+
* a `join` grows. The first is bounded by
|
|
2450
|
+
* {@link WORKFLOW_AGGREGATE_MAX_AGGREGATES}; the second has its own bound on the
|
|
2451
|
+
* aggregate, because it is the one accumulator whose size is not fixed by the
|
|
2452
|
+
* group count.
|
|
2453
|
+
*/
|
|
2454
|
+
export declare const WORKFLOW_AGGREGATE_MAX_GROUPS = 1000000;
|
|
2455
|
+
/**
|
|
2456
|
+
* The highest ceiling an author may ask for.
|
|
2457
|
+
*
|
|
2458
|
+
* Configurable because "how many groups is too many" genuinely depends on the
|
|
2459
|
+
* machine and on how wide the node is, and a hard-coded limit would make the
|
|
2460
|
+
* node unusable for the one legitimate large grouping. Bounded because past this
|
|
2461
|
+
* the answer is not a bigger number — it is that the grouping belongs in the
|
|
2462
|
+
* source query, where the database already has spill-to-disk and this process
|
|
2463
|
+
* does not.
|
|
2464
|
+
*/
|
|
2465
|
+
export declare const WORKFLOW_AGGREGATE_GROUPS_CEILING = 20000000;
|
|
2466
|
+
/**
|
|
2467
|
+
* The default bound on one joined value, in characters.
|
|
2468
|
+
*
|
|
2469
|
+
* 65,535 because that is what a MySQL `TEXT` column holds, and a value the
|
|
2470
|
+
* target column cannot store is the same defect one layer further down. See
|
|
2471
|
+
* `appendJoin` for the full argument, including the five groups per column that
|
|
2472
|
+
* are silently truncated in production today under a limit of 1,024.
|
|
2473
|
+
*/
|
|
2474
|
+
export declare const WORKFLOW_AGGREGATE_JOIN_MAX_LENGTH = 65535;
|
|
2475
|
+
/** The highest an author may raise a `join` bound to. One `MEDIUMTEXT`. */
|
|
2476
|
+
export declare const WORKFLOW_AGGREGATE_JOIN_LENGTH_CEILING = 16777215;
|
|
2477
|
+
/** The longest separator a `join` may use. Long enough for `" | "`, short enough not to be data. */
|
|
2478
|
+
export declare const WORKFLOW_AGGREGATE_MAX_SEPARATOR = 16;
|
|
2479
|
+
/** The separator a `join` uses when the aggregate does not name one. */
|
|
2480
|
+
export declare const WORKFLOW_AGGREGATE_DEFAULT_SEPARATOR = ", ";
|
|
2481
|
+
/**
|
|
2482
|
+
* One computed column: a function, what it reads, and what it is called.
|
|
2483
|
+
*
|
|
2484
|
+
* A list of these rather than a `Record<name, spec>`, which is the opposite of
|
|
2485
|
+
* the shape {@link WorkflowRenameNode.columns} takes, and the reason is that the
|
|
2486
|
+
* two nodes are keyed by different things. A rename is keyed by its **source**
|
|
2487
|
+
* column and a source column can only be renamed once, so an object gets that
|
|
2488
|
+
* refusal for free. An aggregate is keyed by its **output** name, and the same
|
|
2489
|
+
* source column being read by several aggregates — `min(closedDate)` and
|
|
2490
|
+
* `max(closedDate)` — is the normal case rather than the mistake. A list also
|
|
2491
|
+
* keeps the author's order, which is the order the output columns come out in.
|
|
2492
|
+
*
|
|
2493
|
+
* Two aggregates sharing one `as` is the mistake, it is representable, and
|
|
2494
|
+
* {@link aggregateRefusals} names both of them.
|
|
2495
|
+
*/
|
|
2496
|
+
export interface WorkflowAggregate {
|
|
2497
|
+
/**
|
|
2498
|
+
* The output column name. Matches {@link WORKFLOW_FILTER_COLUMN_PATTERN}.
|
|
2499
|
+
*
|
|
2500
|
+
* Constrained for the reason {@link WorkflowRenameNode.columns}' targets are,
|
|
2501
|
+
* and it is the same trap: `property-names.ts` refuses a published property
|
|
2502
|
+
* whose name cannot become a column, a load looks every field up as
|
|
2503
|
+
* `row[name]`, and a name this service cannot carry downstream loads NULL into
|
|
2504
|
+
* every row and reports success.
|
|
2505
|
+
*/
|
|
2506
|
+
as: string;
|
|
2507
|
+
/** Which function. See {@link WORKFLOW_AGGREGATE_FUNCTIONS}. */
|
|
2508
|
+
fn: WorkflowAggregateFunction;
|
|
2509
|
+
/**
|
|
2510
|
+
* The column it reads. Absent is allowed **only** for `count`, where it means
|
|
2511
|
+
* `COUNT(*)` — how many records landed in the group.
|
|
2512
|
+
*
|
|
2513
|
+
* Also matches {@link WORKFLOW_FILTER_COLUMN_PATTERN}, and that is a rule
|
|
2514
|
+
* about *input* rather than output, which the rename node deliberately does
|
|
2515
|
+
* not have. It is here because the filter node already draws the line in the
|
|
2516
|
+
* same place and for the same forward-looking reason: an aggregate that reads
|
|
2517
|
+
* a column no `GROUP BY` could name is one that could never be pushed into the
|
|
2518
|
+
* query the source already runs. A source whose own headers are `Work Order
|
|
2519
|
+
* Id` is what the `rename` node is for, one node upstream.
|
|
2520
|
+
*/
|
|
2521
|
+
column?: string;
|
|
2522
|
+
/**
|
|
2523
|
+
* `join` only. What goes between the values. Absent means
|
|
2524
|
+
* {@link WORKFLOW_AGGREGATE_DEFAULT_SEPARATOR}.
|
|
2525
|
+
*/
|
|
2526
|
+
separator?: string;
|
|
2527
|
+
/**
|
|
2528
|
+
* `join` only. The bound, in characters, that this aggregate refuses at.
|
|
2529
|
+
* Absent means {@link WORKFLOW_AGGREGATE_JOIN_MAX_LENGTH}.
|
|
2530
|
+
*/
|
|
2531
|
+
maxLength?: number;
|
|
2532
|
+
}
|
|
2533
|
+
/**
|
|
2534
|
+
* Groups records and computes a summary of each group. No author code.
|
|
2535
|
+
*
|
|
2536
|
+
* ## Why this is a node kind, and why it stays small
|
|
2537
|
+
*
|
|
2538
|
+
* The argument the `rename` node made, and it holds unchanged: the generic
|
|
2539
|
+
* {@link WorkflowTransformNode} continues to exist, and **that is what lets this
|
|
2540
|
+
* node stay deliberately narrow forever**. "I need more than this" is answered
|
|
2541
|
+
* with *use a transform*, never with *add a field here*. The refusals in this
|
|
2542
|
+
* docblock are the point of it, not an apology.
|
|
2543
|
+
*
|
|
2544
|
+
* What is different from `rename` is that the payoff is not speed, it is a
|
|
2545
|
+
* **memory bound**, and it is the difference between working and being killed.
|
|
2546
|
+
*
|
|
2547
|
+
* ## The measurement
|
|
2548
|
+
*
|
|
2549
|
+
* flip's `wo` table is a `GROUP BY` over 44,720 SUBWO rows producing 16,119
|
|
2550
|
+
* groups with 49 aggregates. Reproduced as a whole-batch transform — which is
|
|
2551
|
+
* the only way this graph can express it today — it works, and only just:
|
|
2552
|
+
*
|
|
2553
|
+
* - the child process is handed **65.51 MiB** on stdin;
|
|
2554
|
+
* - it answers with a single JSON line of **25.01 MiB, which is 78.2% of the
|
|
2555
|
+
* hard 32 MiB output cap**, buffered by the parent as one string;
|
|
2556
|
+
* - and `readInputs` materialises all 44,720 records before the child starts. A
|
|
2557
|
+
* standalone equivalent peaked at 237 MiB heap and 406 MiB RSS.
|
|
2558
|
+
*
|
|
2559
|
+
* **It fails at 1.28× that file** — one more summed column, or one more month of
|
|
2560
|
+
* work orders — and it fails by being killed at the cap rather than by
|
|
2561
|
+
* degrading. SUBWO is already the largest file in the drop it comes from. So the
|
|
2562
|
+
* commonest aggregation in the system is one column away from dying at a
|
|
2563
|
+
* ceiling.
|
|
2564
|
+
*
|
|
2565
|
+
* A hash aggregate consumes its input as a stream and **holds only the groups**:
|
|
2566
|
+
* 44,720 rows in, 16,119 accumulator rows held, one staged batch decoded at a
|
|
2567
|
+
* time. For most real aggregations the group count is far smaller than the row
|
|
2568
|
+
* count, and that gap is the entire feature.
|
|
2569
|
+
*
|
|
2570
|
+
* ## Which is why the bound is loud rather than absent
|
|
2571
|
+
*
|
|
2572
|
+
* "Holds only the groups" stops being cheap the moment the groups approach the
|
|
2573
|
+
* rows. So {@link maxGroups} is a ceiling that **refuses**, naming the columns
|
|
2574
|
+
* being grouped on. Peaking silently is the failure being fixed, and a node that
|
|
2575
|
+
* fixed it by peaking somewhere else would not have fixed anything.
|
|
2576
|
+
*
|
|
2577
|
+
* ## What comes out
|
|
2578
|
+
*
|
|
2579
|
+
* The group-by columns, then one column per aggregate, in the node's order —
|
|
2580
|
+
* **on every record, always, including where the answer is null**. That makes an
|
|
2581
|
+
* aggregate the only node kind whose output column set is *exact* rather than an
|
|
2582
|
+
* upper bound; see `producedColumns`.
|
|
2583
|
+
*
|
|
2584
|
+
* ## Every edge case, decided rather than discovered
|
|
2585
|
+
*
|
|
2586
|
+
* - **A group-by column not present in a record** — that record groups with the
|
|
2587
|
+
* records whose column is null, and the output carries `null`. SQL groups all
|
|
2588
|
+
* NULLs together, and the stage encoding makes absent-versus-null a difference
|
|
2589
|
+
* in physical layout rather than in meaning, so splitting them would make the
|
|
2590
|
+
* answer depend on which shape a row landed in.
|
|
2591
|
+
* - **A null group key** — a group like any other, exactly as `GROUP BY`
|
|
2592
|
+
* produces.
|
|
2593
|
+
* - **`1` and `"1"` as group keys** — *different* groups. SQL would coerce them
|
|
2594
|
+
* together; merging two things the source considered distinct on a rule nobody
|
|
2595
|
+
* chose is the worse error, and within one load a column is one type. See
|
|
2596
|
+
* `groupKeyOf`.
|
|
2597
|
+
* - **Empty input** — no groups, so no rows staged, which is what `GROUP BY`
|
|
2598
|
+
* over an empty table produces. The sink already refuses to commit an empty
|
|
2599
|
+
* full snapshot, so nothing new is needed and nothing is silently green.
|
|
2600
|
+
* - **An empty {@link groupBy}** — *refused*, and it is worth saying why rather
|
|
2601
|
+
* than leaving it as a bound. With no grouping, SQL returns exactly one row
|
|
2602
|
+
* whether the input had a billion rows or none, and a run that commits one row
|
|
2603
|
+
* of nulls looks identical to a run that summarised everything. A grand total
|
|
2604
|
+
* is genuinely wanted sometimes; the way to ask for it is a constant column
|
|
2605
|
+
* from a transform, which makes the one row visible in the graph.
|
|
2606
|
+
* - **Two aggregates writing the same output name** — refused, naming both, from
|
|
2607
|
+
* the config alone with nothing to run.
|
|
2608
|
+
* - **An aggregate whose `as` collides with a group-by column** — refused, same
|
|
2609
|
+
* sentence, same reason: one name, two values, and every rule for picking a
|
|
2610
|
+
* winner is arbitrary.
|
|
2611
|
+
* - **Aggregating a column that does not exist upstream** — refused at *save*
|
|
2612
|
+
* wherever `workflowKnownColumns` can prove it (below a `rename` that drops
|
|
2613
|
+
* what it does not name, or below a `catalog` source when the caller supplied
|
|
2614
|
+
* a column lookup). Where the graph cannot prove it, the run says so out loud:
|
|
2615
|
+
* a column present in **no** record of the whole run is reported, and `sum`,
|
|
2616
|
+
* `min`, `max` and `join` over nothing answer `null` rather than `0` or `""`,
|
|
2617
|
+
* which is visibly different from a real answer.
|
|
2618
|
+
* - **Mixed types in a `min`/`max`, or non-numeric text in a `sum`** — refused
|
|
2619
|
+
* at run time, naming the column, the group and the values. MySQL coerces;
|
|
2620
|
+
* coercing here would make the answer depend on which record arrived first and
|
|
2621
|
+
* the run would still be green.
|
|
2622
|
+
*
|
|
2623
|
+
* ## Ordering, and what is actually promised
|
|
2624
|
+
*
|
|
2625
|
+
* `GROUP_CONCAT` without an `ORDER BY` is unordered, which is why a comparison
|
|
2626
|
+
* against flip found identical-length, different-order strings. This node joins
|
|
2627
|
+
* in **input order** and emits groups in **first-seen order**, both of which are
|
|
2628
|
+
* functions of the numbered list of staged batches it reads. So two runs over
|
|
2629
|
+
* the same staged input produce the same bytes. What is *not* promised is
|
|
2630
|
+
* stability across a source that returns its own rows in a different order — a
|
|
2631
|
+
* `SELECT` without an `ORDER BY` promises nothing, and this node cannot promise
|
|
2632
|
+
* more than it was handed.
|
|
2633
|
+
*/
|
|
2634
|
+
export interface WorkflowAggregateNode extends WorkflowNodeBase {
|
|
2635
|
+
kind: 'aggregate';
|
|
2636
|
+
/**
|
|
2637
|
+
* The columns that define a group. Never empty; at most
|
|
2638
|
+
* {@link WORKFLOW_AGGREGATE_MAX_GROUP_BY}; each matches
|
|
2639
|
+
* {@link WORKFLOW_FILTER_COLUMN_PATTERN}; no duplicates.
|
|
2640
|
+
*
|
|
2641
|
+
* These are output column names as well as input ones — the group key comes
|
|
2642
|
+
* out under the name it went in under — which is why they carry the target
|
|
2643
|
+
* name rule and not only the input one.
|
|
2644
|
+
*/
|
|
2645
|
+
groupBy: string[];
|
|
2646
|
+
/**
|
|
2647
|
+
* What to compute per group. Never empty; at most
|
|
2648
|
+
* {@link WORKFLOW_AGGREGATE_MAX_AGGREGATES}; no two share an `as`.
|
|
2649
|
+
*
|
|
2650
|
+
* Empty is refused rather than treated as "just deduplicate", which is what it
|
|
2651
|
+
* would silently be: a node emitting the distinct combinations of its group-by
|
|
2652
|
+
* columns and dropping every other column of every row. That is a real
|
|
2653
|
+
* operation and a completely different one, and reaching it by deleting the
|
|
2654
|
+
* last row of a form is how a published type loses forty columns.
|
|
2655
|
+
*/
|
|
2656
|
+
aggregates: WorkflowAggregate[];
|
|
2657
|
+
/**
|
|
2658
|
+
* The ceiling on distinct groups. Absent means
|
|
2659
|
+
* {@link WORKFLOW_AGGREGATE_MAX_GROUPS}; at most
|
|
2660
|
+
* {@link WORKFLOW_AGGREGATE_GROUPS_CEILING}.
|
|
2661
|
+
*/
|
|
2662
|
+
maxGroups?: number;
|
|
2663
|
+
}
|
|
2664
|
+
/** {@link WorkflowAggregateNode.maxGroups}, resolved. One reader of the default. */
|
|
2665
|
+
export declare function workflowAggregateMaxGroups(node: WorkflowAggregateNode): number;
|
|
2666
|
+
/** {@link WorkflowAggregate.separator}, resolved. One reader of the default. */
|
|
2667
|
+
export declare function workflowAggregateSeparator(aggregate: WorkflowAggregate): string;
|
|
2668
|
+
/** {@link WorkflowAggregate.maxLength}, resolved. One reader of the default. */
|
|
2669
|
+
export declare function workflowAggregateJoinMaxLength(aggregate: WorkflowAggregate): number;
|
|
2670
|
+
/** Whether this function reads a column. Only `count` may go without one. */
|
|
2671
|
+
export declare function workflowAggregateNeedsColumn(fn: WorkflowAggregateFunction): boolean;
|
|
2672
|
+
/**
|
|
2673
|
+
* The columns an aggregate node **reads**: its group keys and its inputs.
|
|
2674
|
+
*
|
|
2675
|
+
* What `checkColumnsProduced` tests against what the graph can prove is there,
|
|
2676
|
+
* and what the run log reports as never-seen. Deduplicated and in a stable
|
|
2677
|
+
* order, because it goes into a sentence.
|
|
2678
|
+
*/
|
|
2679
|
+
export declare function workflowAggregateColumns(node: WorkflowAggregateNode): string[];
|
|
2680
|
+
/**
|
|
2681
|
+
* The columns an aggregate node **produces**, which is all of them and nothing
|
|
2682
|
+
* else.
|
|
2683
|
+
*
|
|
2684
|
+
* Closed by the config, and closed *exactly* rather than as an upper bound —
|
|
2685
|
+
* every emitted record carries every one of these keys, whatever was upstream
|
|
2686
|
+
* and whatever the values turned out to be. That is a stronger claim than the
|
|
2687
|
+
* one `rename` introduced, and it is stronger for a structural reason: a rename
|
|
2688
|
+
* only produces a target where the input actually held the source column,
|
|
2689
|
+
* whereas an aggregate writes a group's answer whether or not anything in the
|
|
2690
|
+
* group had a value for it.
|
|
2691
|
+
*
|
|
2692
|
+
* The one thing it does not claim is that the values are useful. An aggregate
|
|
2693
|
+
* over a column that no record carried produces the column, holding `null`.
|
|
2694
|
+
*/
|
|
2695
|
+
export declare function workflowAggregateOutputColumns(node: WorkflowAggregateNode): string[];
|
|
2696
|
+
/**
|
|
2697
|
+
* Every reason an aggregate cannot be stored, as sentences, or empty.
|
|
2698
|
+
*
|
|
2699
|
+
* One function, called by {@link validateWorkflow}, by the HTTP boundary, by the
|
|
2700
|
+
* canvas and by the fold itself, for the reason {@link renameColumnRefusals} is
|
|
2701
|
+
* shared: a screen with its own copy of the identifier pattern is a screen that
|
|
2702
|
+
* accepts something the server refuses, halfway through a save.
|
|
2703
|
+
*
|
|
2704
|
+
* All of them rather than the first, exactly as
|
|
2705
|
+
* {@link refuseUnpublishablePropertyNames} argues: a node with forty aggregates
|
|
2706
|
+
* typed in one sitting is usually wrong about several in the same way.
|
|
2707
|
+
*/
|
|
2708
|
+
export declare function aggregateRefusals(node: {
|
|
2709
|
+
groupBy?: unknown;
|
|
2710
|
+
aggregates?: unknown;
|
|
2711
|
+
maxGroups?: unknown;
|
|
2712
|
+
}): string[];
|
|
2713
|
+
/**
|
|
2714
|
+
* Whether a stored aggregate list is one this build can run.
|
|
2715
|
+
*
|
|
2716
|
+
* Refused rather than repaired, the stance {@link isWorkflowRenameColumns} takes
|
|
2717
|
+
* and for the same reason one step further along: an aggregate list read back
|
|
2718
|
+
* with one entry silently dropped is a graph that commits a column of nulls
|
|
2719
|
+
* under a name somebody put in an object type on purpose.
|
|
2720
|
+
*/
|
|
2721
|
+
export declare function isWorkflowAggregates(value: unknown): value is WorkflowAggregate[];
|
|
2307
2722
|
/**
|
|
2308
2723
|
* A discriminated union, so narrowing a node is `node.kind === "sink"` and
|
|
2309
2724
|
* never a type assertion. This is why the kind list is not simply a string on
|
|
2310
2725
|
* one node shape with every field optional: that shape lets a source node carry
|
|
2311
2726
|
* a `transformId` and nothing catches it.
|
|
2312
2727
|
*/
|
|
2313
|
-
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode | WorkflowRenameNode;
|
|
2728
|
+
export type WorkflowNode = WorkflowSourceNode | WorkflowTransformNode | WorkflowSinkNode | WorkflowCallNode | WorkflowIfNode | WorkflowFilterNode | WorkflowRenameNode | WorkflowAggregateNode;
|
|
2314
2729
|
/**
|
|
2315
2730
|
* The node kinds that can be saved once and used in several graphs.
|
|
2316
2731
|
*
|
|
@@ -2367,6 +2782,13 @@ export declare function isReusableNodeKind(value: unknown): value is ReusableNod
|
|
|
2367
2782
|
* *about* one drop of one file. `Mgmt Cd → mgmtCd` saved under a name and
|
|
2368
2783
|
* dropped into a graph reading a different system renames nothing at all, and
|
|
2369
2784
|
* the symptom is a column of NULLs rather than a failure.
|
|
2785
|
+
* - `aggregate` — the same again, and it fails in both directions at once. Its
|
|
2786
|
+
* group-by columns and its inputs name one type's columns, so a shared one
|
|
2787
|
+
* groups a graph it was not written for on a column that is not there — which
|
|
2788
|
+
* collapses every record into one null-keyed group rather than erroring. And
|
|
2789
|
+
* its *output* column set is the thing downstream nodes are validated against,
|
|
2790
|
+
* so a shared node editable from elsewhere would silently change what another
|
|
2791
|
+
* graph's sink is allowed to write.
|
|
2370
2792
|
*/
|
|
2371
2793
|
export declare const NODE_KIND_IS_REUSABLE: {
|
|
2372
2794
|
readonly source: true;
|
|
@@ -2376,6 +2798,7 @@ export declare const NODE_KIND_IS_REUSABLE: {
|
|
|
2376
2798
|
readonly if: false;
|
|
2377
2799
|
readonly filter: false;
|
|
2378
2800
|
readonly rename: false;
|
|
2801
|
+
readonly aggregate: false;
|
|
2379
2802
|
};
|
|
2380
2803
|
/** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
|
|
2381
2804
|
export declare function nodeKindIsReusable(kind: WorkflowNodeKind): boolean;
|
|
@@ -3174,7 +3597,7 @@ export interface CallableWorkflowBlock {
|
|
|
3174
3597
|
}
|
|
3175
3598
|
export declare function callableWorkflowBlock(ref: CallableWorkflowRef): CallableWorkflowBlock | undefined;
|
|
3176
3599
|
/** Every way a graph can be refused. Exported so a canvas can key off the code. */
|
|
3177
|
-
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", "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"];
|
|
3600
|
+
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", "column-not-produced", "version-pin-invalid"];
|
|
3178
3601
|
export type WorkflowIssueCode = (typeof WORKFLOW_ISSUE_CODES)[number];
|
|
3179
3602
|
export interface WorkflowValidationIssue {
|
|
3180
3603
|
code: WorkflowIssueCode;
|
|
@@ -3203,8 +3626,16 @@ export interface WorkflowValidationIssue {
|
|
|
3203
3626
|
* point at nodes which do not exist produces a second page of consequences, and
|
|
3204
3627
|
* burying the one real problem under them is how a validation message stops
|
|
3205
3628
|
* being read.
|
|
3629
|
+
*
|
|
3630
|
+
* `knowledge` is optional and adds only refusals that could not otherwise be
|
|
3631
|
+
* made: with it, a filter or a rename below a `catalog` source can be told it
|
|
3632
|
+
* names a column the published type does not have. Omitting it is a supported
|
|
3633
|
+
* call and the answer is a subset, never a different one — see
|
|
3634
|
+
* {@link WorkflowColumnKnowledge}. The pure, dependency-free promise this
|
|
3635
|
+
* function opens with is unchanged: the lookup is the caller's, and this reaches
|
|
3636
|
+
* nothing.
|
|
3206
3637
|
*/
|
|
3207
|
-
export declare function validateWorkflow(graph: WorkflowGraph): WorkflowValidationIssue[];
|
|
3638
|
+
export declare function validateWorkflow(graph: WorkflowGraph, knowledge?: WorkflowColumnKnowledge): WorkflowValidationIssue[];
|
|
3208
3639
|
/**
|
|
3209
3640
|
* The object types whose whole published snapshot a node stands in front of.
|
|
3210
3641
|
*
|
|
@@ -3371,9 +3802,23 @@ export declare function workflowFilterColumns(predicate: WorkflowFilterPredicate
|
|
|
3371
3802
|
* with some keys re-labelled, and its input is unknown unless something
|
|
3372
3803
|
* upstream closed it. So `undefined` propagates, and that is the honest
|
|
3373
3804
|
* answer rather than an empty set.
|
|
3374
|
-
* - **A
|
|
3375
|
-
*
|
|
3376
|
-
*
|
|
3805
|
+
* - **A transform and a call are always unknown.** A transform is a function
|
|
3806
|
+
* body; a call is a workflow this graph does not own.
|
|
3807
|
+
* - **A source is unknown, with one exception, and the exception needs a
|
|
3808
|
+
* lookup.** Every kind that reaches an outside system has a shape discovered
|
|
3809
|
+
* against that system rather than declared in the graph. A `catalog` source
|
|
3810
|
+
* is the one kind whose shape is *already published*: it names an object
|
|
3811
|
+
* type, and the type's properties are exactly the keys its records carry —
|
|
3812
|
+
* see `fetchCatalog`, which asks the store for those properties by name.
|
|
3813
|
+
*
|
|
3814
|
+
* But the properties are not in the graph either. The graph holds a type
|
|
3815
|
+
* **name**; the columns live in the catalog's registry, which this function is
|
|
3816
|
+
* pure and dependency-free in order not to reach. So the answer is a lookup
|
|
3817
|
+
* the caller supplies — {@link WorkflowColumnKnowledge} — and with no lookup
|
|
3818
|
+
* the answer stays `undefined`. That is the honest shape of the claim: a
|
|
3819
|
+
* caller that can see the catalog gets column checking through a source, which
|
|
3820
|
+
* nothing else in this file can offer, and a caller that cannot see it is told
|
|
3821
|
+
* nothing rather than told an empty set.
|
|
3377
3822
|
* - **It says nothing about a sink's declared properties.** That is the check
|
|
3378
3823
|
* worth wanting — "this sink writes a property no upstream node produces" —
|
|
3379
3824
|
* and it is *not* available here: a {@link WorkflowSinkNode} carries a
|
|
@@ -3385,7 +3830,24 @@ export declare function workflowFilterColumns(predicate: WorkflowFilterPredicate
|
|
|
3385
3830
|
* cyclic graph before it gets here, but the canvas calls this while a graph is
|
|
3386
3831
|
* being drawn and is entitled to a wrong-but-terminating answer.
|
|
3387
3832
|
*/
|
|
3388
|
-
export declare function workflowKnownColumns(graph: WorkflowGraph, nodeId: string): ReadonlySet<string> | undefined;
|
|
3833
|
+
export declare function workflowKnownColumns(graph: WorkflowGraph, nodeId: string, knowledge?: WorkflowColumnKnowledge): ReadonlySet<string> | undefined;
|
|
3834
|
+
/**
|
|
3835
|
+
* What a caller can tell the column walk that the graph does not hold.
|
|
3836
|
+
*
|
|
3837
|
+
* One lookup, because there is one thing: the properties of a published object
|
|
3838
|
+
* type, which is what a `catalog` source's records are keyed by. Optional in
|
|
3839
|
+
* every signature that takes it and `undefined` is a supported answer with its
|
|
3840
|
+
* own meaning — "this console cannot see a type by that name" is not "that type
|
|
3841
|
+
* has no columns", and an empty set would be read as the second.
|
|
3842
|
+
*
|
|
3843
|
+
* A lookup rather than a map, so a caller that already holds the types answers
|
|
3844
|
+
* from what it holds, and one that would have to fetch them can decline by not
|
|
3845
|
+
* passing this at all.
|
|
3846
|
+
*/
|
|
3847
|
+
export interface WorkflowColumnKnowledge {
|
|
3848
|
+
/** The property names of a published type, or nothing if it cannot be seen. */
|
|
3849
|
+
columnsOfType(typeName: string): Iterable<string> | undefined;
|
|
3850
|
+
}
|
|
3389
3851
|
/**
|
|
3390
3852
|
* Narrow a stored node, loudly.
|
|
3391
3853
|
*
|