@dudousxd/nestjs-catalog 0.22.0 → 0.24.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.environment.d.ts +12 -1
- package/dist/catalog.environment.js +12 -1
- package/dist/catalog.pipeline.d.ts +548 -3
- package/dist/catalog.pipeline.js +570 -1
- package/dist/catalog.stage-encoding.d.ts +72 -0
- package/dist/catalog.stage-encoding.js +100 -0
- package/dist/client.d.ts +3 -3
- package/dist/client.js +31 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +21 -4
- package/dist/transform-runner.d.ts +42 -1
- package/dist/transform-runner.js +677 -28
- package/package.json +1 -1
package/dist/catalog.pipeline.js
CHANGED
|
@@ -9,11 +9,16 @@
|
|
|
9
9
|
* systems each believing they decide when a load runs.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.REDACTED_SECRET = exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_BRANCH_LABELS = exports.NODE_KIND_IS_REUSABLE = exports.REUSABLE_NODE_KINDS = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_CALL_MODES = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_SKIP_REASONS = exports.CODE_CONTEXT_CONTRACT = exports.TRANSFORM_RUNNER = exports.TRANSFORM_LANGUAGES = exports.SOURCE_FORMATS = exports.CONNECTOR_KINDS = void 0;
|
|
12
|
+
exports.REDACTED_SECRET = exports.CATALOG_PIPELINE_STORE = exports.WORKFLOW_ISSUE_CODES = exports.WORKFLOW_CALL_CONTRACT = exports.WORKFLOW_EXECUTION_MODES = exports.WORKFLOW_STATUSES = exports.WORKFLOW_BRANCH_LABELS = exports.NODE_KIND_IS_REUSABLE = exports.REUSABLE_NODE_KINDS = exports.WORKFLOW_RENAME_MAX_COLUMNS = exports.WORKFLOW_RENAME_UNNAMED = exports.WORKFLOW_FILTER_MAX_VALUES = exports.WORKFLOW_FILTER_MAX_DEPTH = exports.WORKFLOW_FILTER_COLUMN_PATTERN = exports.WORKFLOW_FILTER_OPERATORS = exports.WORKFLOW_FILTER_PREDICATE_KINDS = exports.WORKFLOW_PREDICATE_KINDS = exports.WORKFLOW_CALL_MODES = exports.WORKFLOW_ROW_GAP = exports.WORKFLOW_COLUMN_GAP = exports.WORKFLOW_NODE_HEIGHT = exports.WORKFLOW_NODE_WIDTH = exports.WORKFLOW_NODE_ID_PATTERN = exports.WORKFLOW_NODE_KINDS = exports.WORKFLOW_SKIP_REASONS = exports.CODE_CONTEXT_CONTRACT = exports.TRANSFORM_RUNNER = exports.TRANSFORM_MODES = exports.TRANSFORM_LANGUAGES = exports.SOURCE_FORMATS = exports.CONNECTOR_KINDS = void 0;
|
|
13
13
|
exports.isConnectorKind = isConnectorKind;
|
|
14
14
|
exports.isSourceFormat = isSourceFormat;
|
|
15
15
|
exports.unreachableSourceFormat = unreachableSourceFormat;
|
|
16
16
|
exports.isTransformLanguage = isTransformLanguage;
|
|
17
|
+
exports.isTransformMode = isTransformMode;
|
|
18
|
+
exports.unreachableTransformMode = unreachableTransformMode;
|
|
19
|
+
exports.transformMode = transformMode;
|
|
20
|
+
exports.recordModeRefusal = recordModeRefusal;
|
|
21
|
+
exports.supportsTransformStreaming = supportsTransformStreaming;
|
|
17
22
|
exports.isWorkflowSkipReason = isWorkflowSkipReason;
|
|
18
23
|
exports.isWorkflowNodeKind = isWorkflowNodeKind;
|
|
19
24
|
exports.unreachableNodeKind = unreachableNodeKind;
|
|
@@ -32,6 +37,11 @@ exports.unreachableFilterOperator = unreachableFilterOperator;
|
|
|
32
37
|
exports.isWorkflowFilterValue = isWorkflowFilterValue;
|
|
33
38
|
exports.isWorkflowFilterPredicate = isWorkflowFilterPredicate;
|
|
34
39
|
exports.workflowFilterMatches = workflowFilterMatches;
|
|
40
|
+
exports.isWorkflowRenameUnnamed = isWorkflowRenameUnnamed;
|
|
41
|
+
exports.unreachableRenameUnnamed = unreachableRenameUnnamed;
|
|
42
|
+
exports.workflowRenameUnnamed = workflowRenameUnnamed;
|
|
43
|
+
exports.renameColumnRefusals = renameColumnRefusals;
|
|
44
|
+
exports.isWorkflowRenameColumns = isWorkflowRenameColumns;
|
|
35
45
|
exports.isReusableNodeKind = isReusableNodeKind;
|
|
36
46
|
exports.nodeKindIsReusable = nodeKindIsReusable;
|
|
37
47
|
exports.unreachableReusableNodeKind = unreachableReusableNodeKind;
|
|
@@ -50,6 +60,8 @@ exports.workflowNarrowedTypes = workflowNarrowedTypes;
|
|
|
50
60
|
exports.workflowRunOrder = workflowRunOrder;
|
|
51
61
|
exports.workflowNodeRuns = workflowNodeRuns;
|
|
52
62
|
exports.workflowGraphHash = workflowGraphHash;
|
|
63
|
+
exports.workflowFilterColumns = workflowFilterColumns;
|
|
64
|
+
exports.workflowKnownColumns = workflowKnownColumns;
|
|
53
65
|
exports.isWorkflowNode = isWorkflowNode;
|
|
54
66
|
exports.isWorkflowEdge = isWorkflowEdge;
|
|
55
67
|
exports.supportsWorkflows = supportsWorkflows;
|
|
@@ -59,6 +71,7 @@ exports.supportsTransformRevisions = supportsTransformRevisions;
|
|
|
59
71
|
exports.supportsTransformPins = supportsTransformPins;
|
|
60
72
|
exports.supportsReusableNodes = supportsReusableNodes;
|
|
61
73
|
exports.supportsWorkflowStages = supportsWorkflowStages;
|
|
74
|
+
exports.supportsStagePayloads = supportsStagePayloads;
|
|
62
75
|
exports.supportsLoadExpectations = supportsLoadExpectations;
|
|
63
76
|
exports.isPipelineStore = isPipelineStore;
|
|
64
77
|
// The revision shape is declared beside the audit trail rather than here,
|
|
@@ -67,6 +80,11 @@ exports.isPipelineStore = isPipelineStore;
|
|
|
67
80
|
// edge only ever points this way: `catalog.workspace.ts` knows nothing about
|
|
68
81
|
// pipelines.
|
|
69
82
|
const catalog_workspace_1 = require("./catalog.workspace");
|
|
83
|
+
// The one rule that tells a module-shaped transform from a bare body, used here
|
|
84
|
+
// to refuse a per-record transform written as a body. The edge only points this
|
|
85
|
+
// way: `transform-shape.ts` imports nothing at all, so it can be read on its own
|
|
86
|
+
// and cannot be dragged into a cycle.
|
|
87
|
+
const transform_shape_1 = require("./transform-shape");
|
|
70
88
|
/**
|
|
71
89
|
* Where a connector pulls from.
|
|
72
90
|
*
|
|
@@ -175,6 +193,128 @@ exports.TRANSFORM_LANGUAGES = ['javascript', 'typescript', 'python'];
|
|
|
175
193
|
function isTransformLanguage(value) {
|
|
176
194
|
return exports.TRANSFORM_LANGUAGES.some((language) => language === value);
|
|
177
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Whether a transform is a function over the whole batch or over one record.
|
|
198
|
+
*
|
|
199
|
+
* ## Why this is declared and never inferred
|
|
200
|
+
*
|
|
201
|
+
* The two are not interchangeable and the difference is invisible in the code.
|
|
202
|
+
* `records.map(...)` and a body that returns one object read almost identically,
|
|
203
|
+
* and a detector that guessed from destructuring — `{ records }` versus
|
|
204
|
+
* `{ record }` — would be reading a *parameter name*, which is the author's to
|
|
205
|
+
* choose and which minification, a rename, or a rest parameter changes without
|
|
206
|
+
* changing what the function computes. Guess wrong towards `record` and an
|
|
207
|
+
* aggregation is called 102,520 times and returns 102,520 partial answers, none
|
|
208
|
+
* of which fails; guess wrong towards `batch` and a per-record function is handed
|
|
209
|
+
* an array and reads `undefined` off every property. Both commit. Neither errors.
|
|
210
|
+
* So the mode is a field somebody set, and the cost of setting it is one control
|
|
211
|
+
* in the editor.
|
|
212
|
+
*
|
|
213
|
+
* ## Why a closed list rather than `streaming?: boolean`
|
|
214
|
+
*
|
|
215
|
+
* The identical argument {@link WORKFLOW_CALL_MODES} makes one level up. A flag
|
|
216
|
+
* beside a future third calling convention — a windowed transform, a keyed one —
|
|
217
|
+
* is two optional booleans whose combinations nobody defined, and each reader
|
|
218
|
+
* invents its own rule for which wins. A closed list with an exhaustiveness guard
|
|
219
|
+
* ({@link unreachableTransformMode}) makes a third convention a compile error
|
|
220
|
+
* naming the files that have to answer for it: the harness that generates the
|
|
221
|
+
* call, the runner that chooses a transport, and the two runners that consume
|
|
222
|
+
* the result.
|
|
223
|
+
*
|
|
224
|
+
* ## What the default has to be, and why it is not a choice
|
|
225
|
+
*
|
|
226
|
+
* Absent means {@link CatalogTransform.mode} was never set, which is every
|
|
227
|
+
* transform stored before this field existed, and every one of them is a function
|
|
228
|
+
* over the whole batch — the harness handed it `records` and there was no other
|
|
229
|
+
* shape to write. Reading absence as anything else would silently change what a
|
|
230
|
+
* deployment's existing loads compute. Read it through {@link transformMode}
|
|
231
|
+
* rather than defaulting it a second time.
|
|
232
|
+
*/
|
|
233
|
+
exports.TRANSFORM_MODES = [
|
|
234
|
+
/**
|
|
235
|
+
* The code is called **once**, with every record the node received.
|
|
236
|
+
*
|
|
237
|
+
* What every stored transform is, and what aggregating, deduplicating,
|
|
238
|
+
* sorting and joining require: none of them can be done a row at a time, and
|
|
239
|
+
* chunking the calls would not fail — it would return one answer per chunk and
|
|
240
|
+
* commit. The whole input is therefore in the running process's heap, which is
|
|
241
|
+
* the honest cost of the promise.
|
|
242
|
+
*/
|
|
243
|
+
'batch',
|
|
244
|
+
/**
|
|
245
|
+
* The code is called **once per record**, and never sees the batch.
|
|
246
|
+
*
|
|
247
|
+
* What a rename, a projection or a normalisation is by construction — and,
|
|
248
|
+
* since `WORKFLOW_FILTER_COLUMN_PATTERN` and `property-names.ts` both refuse a
|
|
249
|
+
* header with a space in it, what every DPAS file this catalog ingests needs
|
|
250
|
+
* before anything else can touch it. The records arrive as a stream and the
|
|
251
|
+
* rows leave as one, so nothing anywhere holds the dataset.
|
|
252
|
+
*/
|
|
253
|
+
'record',
|
|
254
|
+
];
|
|
255
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
256
|
+
function isTransformMode(value) {
|
|
257
|
+
return exports.TRANSFORM_MODES.some((mode) => mode === value);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* {@link unreachableCallMode}, for transforms, and for the identical reason.
|
|
261
|
+
*
|
|
262
|
+
* Every branch over {@link TransformMode} ends here, so a third calling
|
|
263
|
+
* convention added to the list without a harness to generate it, a transport to
|
|
264
|
+
* carry it and a consumer to read its output is a type error naming the file. It
|
|
265
|
+
* throws as well, because a mode arrives as JSON out of a column and a build
|
|
266
|
+
* older than the data is a thing that happens.
|
|
267
|
+
*/
|
|
268
|
+
function unreachableTransformMode(mode, where) {
|
|
269
|
+
throw new Error(`${where} has no rule for the transform mode ${JSON.stringify(mode)}. It was added to TRANSFORM_MODES without teaching this code how to call user code written for it, and guessing would run somebody's transform under a contract they did not write it against.`);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The mode this transform runs in, with the default applied once.
|
|
273
|
+
*
|
|
274
|
+
* Absent means `'batch'` — see {@link TRANSFORM_MODES}. One function so that the
|
|
275
|
+
* store, the runner, the two consumers, the editor and the try pane cannot each
|
|
276
|
+
* carry their own `?? 'batch'` and have one of them drift.
|
|
277
|
+
*/
|
|
278
|
+
function transformMode(transform) {
|
|
279
|
+
return transform.mode ?? 'batch';
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Why this transform cannot run in the mode it declares, if it cannot.
|
|
283
|
+
*
|
|
284
|
+
* Two combinations are representable and neither can work, so both are refused
|
|
285
|
+
* at the point somebody presses save rather than at three in the morning when a
|
|
286
|
+
* schedule fires. `undefined` means there is nothing wrong.
|
|
287
|
+
*
|
|
288
|
+
* Asked in both places on purpose. The controller asks so the author is told
|
|
289
|
+
* while they are still looking at the code; the runner asks because a row can
|
|
290
|
+
* reach it that no controller in this build ever validated — promoted from
|
|
291
|
+
* another environment, restored from a backup, written by an older version — and
|
|
292
|
+
* the failure a runner must never have is the silent one where a per-record
|
|
293
|
+
* module is handed an array and quietly reads `undefined` off every property.
|
|
294
|
+
*/
|
|
295
|
+
function recordModeRefusal(transform) {
|
|
296
|
+
if (transformMode(transform) !== 'record')
|
|
297
|
+
return undefined;
|
|
298
|
+
if (transform.language === 'python') {
|
|
299
|
+
return 'A per-record transform cannot be written in Python yet. The Python harness writes `def transform(records, context):` around the code, so a Python transform never states its own signature and there is no second `def` for the per-record shape — see TRANSFORM_MODES. Use javascript or typescript for a per-record transform, or leave this one as a whole-batch transform.';
|
|
300
|
+
}
|
|
301
|
+
if ((0, transform_shape_1.transformShape)(transform.code) === 'body') {
|
|
302
|
+
return 'A per-record transform must be a module that exports a function — `export default function transform({ record, context }) { … }` — and this code has no `export` at the start of a statement, so the catalog would run it as a bare function body. A body is handed `records`, the whole array, by the wrapper the catalog writes around it; there is no honest way to give it one record under a name it never wrote. Add the `export`, or set this transform back to whole-batch.';
|
|
303
|
+
}
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Whether this runner can stream a per-record transform.
|
|
308
|
+
*
|
|
309
|
+
* The method rather than a flag, exactly as {@link supportsTransformRevisions}
|
|
310
|
+
* argues one interface along: a flag is a claim and a method is the thing
|
|
311
|
+
* itself. A runner that answers `false` still runs `'record'` transforms — the
|
|
312
|
+
* consumers buffer and call {@link TransformRunner.run} — so this is a question
|
|
313
|
+
* about *how much is held*, never about whether the load works.
|
|
314
|
+
*/
|
|
315
|
+
function supportsTransformStreaming(runner) {
|
|
316
|
+
return typeof runner.runStream === 'function';
|
|
317
|
+
}
|
|
178
318
|
exports.TRANSFORM_RUNNER = Symbol('TRANSFORM_RUNNER');
|
|
179
319
|
/**
|
|
180
320
|
* The number in {@link CatalogCodeContext.contract}.
|
|
@@ -296,6 +436,8 @@ exports.WORKFLOW_NODE_KINDS = [
|
|
|
296
436
|
'if',
|
|
297
437
|
/** Drops the rows that fail a declarative test. See {@link WorkflowFilterNode}. */
|
|
298
438
|
'filter',
|
|
439
|
+
/** Renames columns, declaratively. See {@link WorkflowRenameNode}. */
|
|
440
|
+
'rename',
|
|
299
441
|
];
|
|
300
442
|
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
301
443
|
function isWorkflowNodeKind(value) {
|
|
@@ -960,6 +1102,116 @@ function orderedHolds(operator, held, wanted) {
|
|
|
960
1102
|
return held < wanted;
|
|
961
1103
|
return held <= wanted;
|
|
962
1104
|
}
|
|
1105
|
+
/* --- rename -------------------------------------------------------------- */
|
|
1106
|
+
/**
|
|
1107
|
+
* What happens to a column the rename does not name.
|
|
1108
|
+
*
|
|
1109
|
+
* Two words rather than a boolean, because the two are genuinely different
|
|
1110
|
+
* nodes and a boolean called `drop` would read as a modifier on one node. See
|
|
1111
|
+
* {@link WorkflowRenameNode.unnamed} for what each costs.
|
|
1112
|
+
*/
|
|
1113
|
+
exports.WORKFLOW_RENAME_UNNAMED = [
|
|
1114
|
+
/** Passed through untouched. The default, and the metadata-only case. */
|
|
1115
|
+
'keep',
|
|
1116
|
+
/** Removed. The output columns are exactly the map's targets. */
|
|
1117
|
+
'drop',
|
|
1118
|
+
];
|
|
1119
|
+
/** Same reason as {@link isConnectorKind}: one list, no second copy to drift. */
|
|
1120
|
+
function isWorkflowRenameUnnamed(value) {
|
|
1121
|
+
return exports.WORKFLOW_RENAME_UNNAMED.some((each) => each === value);
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* The exhaustiveness guard for {@link WORKFLOW_RENAME_UNNAMED}.
|
|
1125
|
+
*
|
|
1126
|
+
* {@link unreachableNodeKind}, one level down, and for the identical reason: the
|
|
1127
|
+
* two words decide whether a batch is rewritten or only re-labelled, and a third
|
|
1128
|
+
* one added without a branch would silently pick whichever the last `if` was.
|
|
1129
|
+
*/
|
|
1130
|
+
function unreachableRenameUnnamed(value, where) {
|
|
1131
|
+
throw new Error(`${where} does not handle the rename disposition ${JSON.stringify(value)}. The list and every decision made per entry are meant to move together.`);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* How many columns one rename may name.
|
|
1135
|
+
*
|
|
1136
|
+
* The same argument {@link WORKFLOW_FILTER_MAX_VALUES} makes: the map travels in
|
|
1137
|
+
* the graph and into the graph fingerprint, and past a few hundred entries the
|
|
1138
|
+
* thing being expressed is a schema mapping that belongs in a stored object
|
|
1139
|
+
* rather than in a node. It is also the bound that keeps
|
|
1140
|
+
* {@link isWorkflowRenameColumns} — which is run on JSON out of a column — from
|
|
1141
|
+
* being a place to hand a service a million-key object.
|
|
1142
|
+
*/
|
|
1143
|
+
exports.WORKFLOW_RENAME_MAX_COLUMNS = 500;
|
|
1144
|
+
/** {@link WorkflowRenameNode.unnamed}, resolved. One reader of the default. */
|
|
1145
|
+
function workflowRenameUnnamed(node) {
|
|
1146
|
+
return node.unnamed ?? 'keep';
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Every reason a rename map cannot be stored, as sentences, or empty.
|
|
1150
|
+
*
|
|
1151
|
+
* One function, called by {@link validateWorkflow}, by the HTTP boundary and by
|
|
1152
|
+
* the canvas, for the reason `validateWorkflow` itself is shared: a screen that
|
|
1153
|
+
* checked a target name against its own copy of the pattern is a screen that
|
|
1154
|
+
* eventually accepts something the server refuses, halfway through a save.
|
|
1155
|
+
*
|
|
1156
|
+
* All of them rather than the first, exactly as
|
|
1157
|
+
* {@link refuseUnpublishablePropertyNames} argues: a map of forty columns typed
|
|
1158
|
+
* in one sitting is usually wrong about several in the same way.
|
|
1159
|
+
*/
|
|
1160
|
+
function renameColumnRefusals(columns) {
|
|
1161
|
+
const refusals = [];
|
|
1162
|
+
const entries = Object.entries(columns);
|
|
1163
|
+
if (entries.length === 0) {
|
|
1164
|
+
refusals.push('It renames nothing. An empty map is refused rather than stored: with unnamed columns kept it is a node that draws as configured and does nothing, and with them dropped it deletes every column of every row.');
|
|
1165
|
+
return refusals;
|
|
1166
|
+
}
|
|
1167
|
+
if (entries.length > exports.WORKFLOW_RENAME_MAX_COLUMNS) {
|
|
1168
|
+
refusals.push(`It names ${entries.length} columns, and at most ${exports.WORKFLOW_RENAME_MAX_COLUMNS} may be renamed in one node. Past that the thing being expressed is a schema mapping rather than a rename.`);
|
|
1169
|
+
}
|
|
1170
|
+
const targets = new Map();
|
|
1171
|
+
for (const [from, to] of entries) {
|
|
1172
|
+
if (from.length === 0) {
|
|
1173
|
+
refusals.push('One entry renames a column with no name, so there is nothing for it to find.');
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof to !== 'string' || !exports.WORKFLOW_FILTER_COLUMN_PATTERN.test(to)) {
|
|
1177
|
+
refusals.push(`${JSON.stringify(from)} is renamed to ${JSON.stringify(to)}, which is not a name a column can have: letters, digits and underscore, starting with a letter or an underscore. A load looks every field up as \`row[name]\`, so a column this service cannot name downstream is one that loads NULL into every row and reports success.`);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
targets.set(to, [...(targets.get(to) ?? []), from]);
|
|
1181
|
+
}
|
|
1182
|
+
for (const [to, sources] of targets) {
|
|
1183
|
+
if (sources.length < 2)
|
|
1184
|
+
continue;
|
|
1185
|
+
refusals.push(`${sources.map((from) => JSON.stringify(from)).join(' and ')} are both renamed to ${JSON.stringify(to)}. Two columns cannot share one name, and picking a winner would be a rule about which of somebody's data survives.`);
|
|
1186
|
+
}
|
|
1187
|
+
return refusals;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Whether a stored rename map is one this build can run.
|
|
1191
|
+
*
|
|
1192
|
+
* Refused rather than repaired, the stance {@link isWorkflowFilterPredicate}
|
|
1193
|
+
* takes about a predicate and for the same reason one step further along: a
|
|
1194
|
+
* rename read back with one entry silently dropped is a graph that commits a
|
|
1195
|
+
* column of NULLs under a name nobody can now explain.
|
|
1196
|
+
*
|
|
1197
|
+
* `Object.entries` rather than a `for…in`, so an inherited key cannot enter the
|
|
1198
|
+
* map, and the values are checked one by one rather than trusted from the type.
|
|
1199
|
+
*/
|
|
1200
|
+
function isWorkflowRenameColumns(value) {
|
|
1201
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
1202
|
+
return false;
|
|
1203
|
+
const entries = Object.entries(value);
|
|
1204
|
+
if (entries.every(([, to]) => typeof to === 'string')) {
|
|
1205
|
+
const columns = {};
|
|
1206
|
+
for (const [from, to] of entries) {
|
|
1207
|
+
if (typeof to !== 'string')
|
|
1208
|
+
return false;
|
|
1209
|
+
columns[from] = to;
|
|
1210
|
+
}
|
|
1211
|
+
return renameColumnRefusals(columns).length === 0;
|
|
1212
|
+
}
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
963
1215
|
/* --- reusable nodes ------------------------------------------------------ */
|
|
964
1216
|
/**
|
|
965
1217
|
* The node kinds that can be saved once and used in several graphs.
|
|
@@ -1013,6 +1265,11 @@ function isReusableNodeKind(value) {
|
|
|
1013
1265
|
* may not have, and a filter is worse: {@link WorkflowFilterNode.narrows} is
|
|
1014
1266
|
* an acknowledgement about *this* graph's sinks, so a shared one would carry
|
|
1015
1267
|
* somebody else's acknowledgement into a graph they never saw.
|
|
1268
|
+
* - `rename` — the same argument as `if` and `filter`, and the sharpest version
|
|
1269
|
+
* of it: a rename map names the source's own spelling of its columns, so it is
|
|
1270
|
+
* *about* one drop of one file. `Mgmt Cd → mgmtCd` saved under a name and
|
|
1271
|
+
* dropped into a graph reading a different system renames nothing at all, and
|
|
1272
|
+
* the symptom is a column of NULLs rather than a failure.
|
|
1016
1273
|
*/
|
|
1017
1274
|
exports.NODE_KIND_IS_REUSABLE = {
|
|
1018
1275
|
source: true,
|
|
@@ -1021,6 +1278,7 @@ exports.NODE_KIND_IS_REUSABLE = {
|
|
|
1021
1278
|
call: false,
|
|
1022
1279
|
if: false,
|
|
1023
1280
|
filter: false,
|
|
1281
|
+
rename: false,
|
|
1024
1282
|
};
|
|
1025
1283
|
/** Whether this kind can be saved as a reusable node. Reads {@link NODE_KIND_IS_REUSABLE}. */
|
|
1026
1284
|
function nodeKindIsReusable(kind) {
|
|
@@ -1425,6 +1683,29 @@ exports.WORKFLOW_ISSUE_CODES = [
|
|
|
1425
1683
|
'filter-predicate-invalid',
|
|
1426
1684
|
'filter-narrows-unacknowledged',
|
|
1427
1685
|
'filter-narrows-nothing',
|
|
1686
|
+
/**
|
|
1687
|
+
* A rename whose map cannot be stored: empty, too big, or naming a target that
|
|
1688
|
+
* is not a column name or that two source columns share. Every one of those is
|
|
1689
|
+
* detectable from the node alone, which is the point of the node — see
|
|
1690
|
+
* {@link renameColumnRefusals} for the sentences.
|
|
1691
|
+
*/
|
|
1692
|
+
'rename-invalid',
|
|
1693
|
+
/**
|
|
1694
|
+
* A node naming a column that nothing upstream can produce.
|
|
1695
|
+
*
|
|
1696
|
+
* The one thing a declarative rename buys the *validator*, and it is the whole
|
|
1697
|
+
* reason the node is data rather than code. A rename with
|
|
1698
|
+
* `unnamed: 'drop'` has an output column set that is known exactly from its
|
|
1699
|
+
* config — its targets, and nothing else, whatever it was handed. So a filter
|
|
1700
|
+
* or a second rename downstream of one, naming a column outside that set, is
|
|
1701
|
+
* provably wrong at the moment the graph is saved instead of at the moment the
|
|
1702
|
+
* load comes out empty.
|
|
1703
|
+
*
|
|
1704
|
+
* Reported only where the set is *closed*. See {@link workflowKnownColumns}
|
|
1705
|
+
* for exactly how far that reaches and for what it deliberately does not
|
|
1706
|
+
* claim.
|
|
1707
|
+
*/
|
|
1708
|
+
'column-not-produced',
|
|
1428
1709
|
/**
|
|
1429
1710
|
* A version pin that is not a version — `0`, `2.5`, `"3"`, `-1`.
|
|
1430
1711
|
*
|
|
@@ -1496,6 +1777,9 @@ function validateWorkflow(graph) {
|
|
|
1496
1777
|
return issues;
|
|
1497
1778
|
}
|
|
1498
1779
|
checkReachability(nodes, roots, sinks, incoming, outgoing, issues);
|
|
1780
|
+
// After the cycle check has returned, so the walk it does cannot meet a loop
|
|
1781
|
+
// on a graph this function has already accepted as acyclic.
|
|
1782
|
+
checkColumnsProduced({ nodes, edges }, issues);
|
|
1499
1783
|
return issues;
|
|
1500
1784
|
}
|
|
1501
1785
|
/**
|
|
@@ -1851,8 +2135,31 @@ function nodeIsUnconfigured(node) {
|
|
|
1851
2135
|
return ifIsUnconfigured(node);
|
|
1852
2136
|
if (node.kind === 'filter')
|
|
1853
2137
|
return filterIsUnconfigured(node);
|
|
2138
|
+
if (node.kind === 'rename')
|
|
2139
|
+
return renameIsUnconfigured(node);
|
|
1854
2140
|
return undefined;
|
|
1855
2141
|
}
|
|
2142
|
+
/**
|
|
2143
|
+
* A rename whose map this service will not store.
|
|
2144
|
+
*
|
|
2145
|
+
* The refusals come from {@link renameColumnRefusals} rather than being restated
|
|
2146
|
+
* here, so the canvas, the HTTP boundary and this validator say the same
|
|
2147
|
+
* sentence about the same map. Every one of them is a *silent* failure if it
|
|
2148
|
+
* were let through: an empty map is a node that does nothing or a node that
|
|
2149
|
+
* deletes every column, a target that is not a column name loads NULL into every
|
|
2150
|
+
* row and reports success, and two columns renamed onto one name means one of
|
|
2151
|
+
* them is gone and nothing says which.
|
|
2152
|
+
*/
|
|
2153
|
+
function renameIsUnconfigured(node) {
|
|
2154
|
+
const refusals = renameColumnRefusals(node.columns ?? {});
|
|
2155
|
+
if (refusals.length === 0)
|
|
2156
|
+
return undefined;
|
|
2157
|
+
return {
|
|
2158
|
+
code: 'rename-invalid',
|
|
2159
|
+
nodeIds: [node.id],
|
|
2160
|
+
message: `Rename "${node.name}" (${node.id}) cannot be stored as it is. ${refusals.join(' ')}`,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
1856
2163
|
/**
|
|
1857
2164
|
* A filter whose test cannot decide anything.
|
|
1858
2165
|
*
|
|
@@ -2440,6 +2747,8 @@ function canonicalNode(node) {
|
|
|
2440
2747
|
[...(node.narrows ?? [])].sort(),
|
|
2441
2748
|
]);
|
|
2442
2749
|
}
|
|
2750
|
+
if (node.kind === 'rename')
|
|
2751
|
+
return canonicalRename(node);
|
|
2443
2752
|
if (node.kind === 'sink') {
|
|
2444
2753
|
return JSON.stringify([
|
|
2445
2754
|
node.id,
|
|
@@ -2451,6 +2760,28 @@ function canonicalNode(node) {
|
|
|
2451
2760
|
}
|
|
2452
2761
|
return unreachableNodeKind(node, 'workflowGraphHash');
|
|
2453
2762
|
}
|
|
2763
|
+
/**
|
|
2764
|
+
* A rename, canonicalised.
|
|
2765
|
+
*
|
|
2766
|
+
* Sorted by source column, so a canvas that rewrites the object in a different
|
|
2767
|
+
* order is not an edit — the rule `sortedEntries` applies to a source's config,
|
|
2768
|
+
* and it is safe here for a reason specific to this node: the map is applied
|
|
2769
|
+
* *simultaneously*, so its order changes nothing about the result.
|
|
2770
|
+
*
|
|
2771
|
+
* `unnamed` is appended only when it is `drop`, exactly as `edge.branch` is
|
|
2772
|
+
* appended only when there is a label. Every rename that keeps its unnamed
|
|
2773
|
+
* columns — whether it says so or says nothing — hashes to one string, so
|
|
2774
|
+
* normalising the field on a canvas cannot renumber a graph. It is in there at
|
|
2775
|
+
* all because it decides which columns reach the sink.
|
|
2776
|
+
*/
|
|
2777
|
+
function canonicalRename(node) {
|
|
2778
|
+
return JSON.stringify([
|
|
2779
|
+
node.id,
|
|
2780
|
+
node.kind,
|
|
2781
|
+
sortedEntries(node.columns),
|
|
2782
|
+
...(workflowRenameUnnamed(node) === 'drop' ? ['drop'] : []),
|
|
2783
|
+
]);
|
|
2784
|
+
}
|
|
2454
2785
|
/**
|
|
2455
2786
|
* The reusable reference, as zero, one or two trailing hash components.
|
|
2456
2787
|
*
|
|
@@ -2552,6 +2883,207 @@ function canonicalFilterPredicate(predicate) {
|
|
|
2552
2883
|
}
|
|
2553
2884
|
return unreachableFilterPredicateKind(predicate, 'workflowGraphHash');
|
|
2554
2885
|
}
|
|
2886
|
+
/* --- what the graph knows about columns ---------------------------------- */
|
|
2887
|
+
/**
|
|
2888
|
+
* Every column a filter predicate names, once each, in the order they appear.
|
|
2889
|
+
*
|
|
2890
|
+
* Its own function rather than a walk inlined into the validator, because two
|
|
2891
|
+
* things want it — the refusal below and anything on a screen that wants to say
|
|
2892
|
+
* which columns a node depends on — and a second copy of a tree walk is a second
|
|
2893
|
+
* copy that forgets the `oneOf` branch.
|
|
2894
|
+
*/
|
|
2895
|
+
function workflowFilterColumns(predicate) {
|
|
2896
|
+
const found = [];
|
|
2897
|
+
const visit = (each) => {
|
|
2898
|
+
if (each.kind === 'all' || each.kind === 'any') {
|
|
2899
|
+
for (const child of each.children)
|
|
2900
|
+
visit(child);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
if (!found.includes(each.column))
|
|
2904
|
+
found.push(each.column);
|
|
2905
|
+
};
|
|
2906
|
+
visit(predicate);
|
|
2907
|
+
return found;
|
|
2908
|
+
}
|
|
2909
|
+
/**
|
|
2910
|
+
* The columns that can reach this node, when the graph knows — and `undefined`
|
|
2911
|
+
* when it does not.
|
|
2912
|
+
*
|
|
2913
|
+
* ## What this is for
|
|
2914
|
+
*
|
|
2915
|
+
* It is the one thing a declarative rename buys that a transform cannot, and it
|
|
2916
|
+
* is worth being precise about how far it reaches rather than overselling it.
|
|
2917
|
+
*
|
|
2918
|
+
* With a JS transform, the catalog cannot know what columns come out — the
|
|
2919
|
+
* answer is inside a function body — which is why the property-name rule in
|
|
2920
|
+
* `property-names.ts` fires at publish time and why a mismatch between a
|
|
2921
|
+
* property and a record key is discovered as a column of NULLs. A rename is
|
|
2922
|
+
* **data**, so for one arrangement the answer is exact:
|
|
2923
|
+
*
|
|
2924
|
+
* > A rename with `unnamed: 'drop'` produces its targets and **nothing else**,
|
|
2925
|
+
* > whatever it was handed.
|
|
2926
|
+
*
|
|
2927
|
+
* That set is *closed* — an upper bound that holds regardless of what is
|
|
2928
|
+
* upstream — and it survives every node that does not touch columns. So a filter
|
|
2929
|
+
* or a second rename downstream of one can be told, at authoring time, that it
|
|
2930
|
+
* names a column which cannot be there.
|
|
2931
|
+
*
|
|
2932
|
+
* ## What it deliberately does not claim
|
|
2933
|
+
*
|
|
2934
|
+
* - **It is an upper bound, not the output.** A target only appears in a row
|
|
2935
|
+
* whose input actually held the source column. So a column *inside* the set
|
|
2936
|
+
* may still be absent, and nothing here says otherwise.
|
|
2937
|
+
* - **A `keep` rename tells you nothing on its own.** Its output is its input
|
|
2938
|
+
* with some keys re-labelled, and its input is unknown unless something
|
|
2939
|
+
* upstream closed it. So `undefined` propagates, and that is the honest
|
|
2940
|
+
* answer rather than an empty set.
|
|
2941
|
+
* - **A source, a transform and a call are always unknown.** A source's shape is
|
|
2942
|
+
* discovered against the live system rather than declared in the graph; a
|
|
2943
|
+
* transform is a function body; a call is a workflow this graph does not own.
|
|
2944
|
+
* - **It says nothing about a sink's declared properties.** That is the check
|
|
2945
|
+
* worth wanting — "this sink writes a property no upstream node produces" —
|
|
2946
|
+
* and it is *not* available here: a {@link WorkflowSinkNode} carries a
|
|
2947
|
+
* `targetType` and nothing else, so the property list would have to be
|
|
2948
|
+
* threaded into a validator that is pure and dependency-free on purpose. What
|
|
2949
|
+
* is built instead is the run log, which prints the columns a rename produced.
|
|
2950
|
+
*
|
|
2951
|
+
* Cycles answer `undefined` rather than looping. `validateWorkflow` refuses a
|
|
2952
|
+
* cyclic graph before it gets here, but the canvas calls this while a graph is
|
|
2953
|
+
* being drawn and is entitled to a wrong-but-terminating answer.
|
|
2954
|
+
*/
|
|
2955
|
+
function workflowKnownColumns(graph, nodeId) {
|
|
2956
|
+
const nodes = graph.nodes ?? [];
|
|
2957
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
2958
|
+
const { incoming } = buildAdjacency(nodes, graph.edges ?? []);
|
|
2959
|
+
const answered = new Map();
|
|
2960
|
+
const open = new Set();
|
|
2961
|
+
const outOf = (id) => {
|
|
2962
|
+
if (answered.has(id))
|
|
2963
|
+
return answered.get(id);
|
|
2964
|
+
// A loop. Answered as unknown and remembered, so the walk terminates and so
|
|
2965
|
+
// that a second question about the same node does not re-enter it.
|
|
2966
|
+
if (open.has(id))
|
|
2967
|
+
return undefined;
|
|
2968
|
+
const node = byId.get(id);
|
|
2969
|
+
if (!node)
|
|
2970
|
+
return undefined;
|
|
2971
|
+
open.add(id);
|
|
2972
|
+
const produced = producedColumns(node, () => intoNode(id));
|
|
2973
|
+
open.delete(id);
|
|
2974
|
+
answered.set(id, produced);
|
|
2975
|
+
return produced;
|
|
2976
|
+
};
|
|
2977
|
+
const intoNode = (id) => {
|
|
2978
|
+
const feeds = incoming.get(id) ?? [];
|
|
2979
|
+
if (feeds.length === 0)
|
|
2980
|
+
return undefined;
|
|
2981
|
+
const union = new Set();
|
|
2982
|
+
for (const from of feeds) {
|
|
2983
|
+
const upstream = outOf(from);
|
|
2984
|
+
// One unknown input makes the whole position unknown: the rows arrive
|
|
2985
|
+
// concatenated, so a column any one of them carries is a column this node
|
|
2986
|
+
// can see.
|
|
2987
|
+
if (upstream === undefined)
|
|
2988
|
+
return undefined;
|
|
2989
|
+
for (const column of upstream)
|
|
2990
|
+
union.add(column);
|
|
2991
|
+
}
|
|
2992
|
+
return union;
|
|
2993
|
+
};
|
|
2994
|
+
return intoNode(nodeId);
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* What one node passes on, given what reaches it.
|
|
2998
|
+
*
|
|
2999
|
+
* The upstream set is a thunk rather than a value because the one case that
|
|
3000
|
+
* makes this function worth having does not need it: a rename that drops its
|
|
3001
|
+
* unnamed columns answers from its own config, so the walk stops there rather
|
|
3002
|
+
* than climbing to a source it would learn nothing from.
|
|
3003
|
+
*
|
|
3004
|
+
* Ends in {@link unreachableNodeKind}, so a node kind added without an answer
|
|
3005
|
+
* here is a compile error rather than a silent `undefined` — which would be the
|
|
3006
|
+
* *safe* wrong answer and would therefore never be noticed.
|
|
3007
|
+
*/
|
|
3008
|
+
function producedColumns(node, upstream) {
|
|
3009
|
+
if (node.kind === 'rename') {
|
|
3010
|
+
if (workflowRenameUnnamed(node) === 'drop')
|
|
3011
|
+
return new Set(Object.values(node.columns ?? {}));
|
|
3012
|
+
const known = upstream();
|
|
3013
|
+
if (known === undefined)
|
|
3014
|
+
return undefined;
|
|
3015
|
+
const renamed = new Set();
|
|
3016
|
+
for (const column of known)
|
|
3017
|
+
renamed.add(node.columns?.[column] ?? column);
|
|
3018
|
+
return renamed;
|
|
3019
|
+
}
|
|
3020
|
+
// Neither of these touches a column: a filter decides which *rows* survive and
|
|
3021
|
+
// an `if` decides which *nodes* run. Both hand on exactly the shape they were
|
|
3022
|
+
// given, which is what makes a closed set survive one.
|
|
3023
|
+
if (node.kind === 'filter' || node.kind === 'if')
|
|
3024
|
+
return upstream();
|
|
3025
|
+
// A source's shape is discovered against the live system, a transform's is
|
|
3026
|
+
// inside a function body, a call's belongs to a workflow this graph does not
|
|
3027
|
+
// own, and nothing reads a sink's output. See {@link workflowKnownColumns}.
|
|
3028
|
+
if (node.kind === 'source' ||
|
|
3029
|
+
node.kind === 'transform' ||
|
|
3030
|
+
node.kind === 'call' ||
|
|
3031
|
+
node.kind === 'sink') {
|
|
3032
|
+
return undefined;
|
|
3033
|
+
}
|
|
3034
|
+
return unreachableNodeKind(node, 'workflowKnownColumns');
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* That no node names a column the graph can prove is not there.
|
|
3038
|
+
*
|
|
3039
|
+
* Only where {@link workflowKnownColumns} answers, which is only downstream of a
|
|
3040
|
+
* rename that drops what it does not name. Everywhere else this is silent, and
|
|
3041
|
+
* that silence is correct rather than a gap being tolerated: refusing a column
|
|
3042
|
+
* the graph merely has no opinion about would make every filter downstream of a
|
|
3043
|
+
* transform unsaveable.
|
|
3044
|
+
*
|
|
3045
|
+
* A refusal rather than a warning, because both failures are silent and total.
|
|
3046
|
+
* A filter on a column that cannot exist matches no row — a comparison against
|
|
3047
|
+
* an absent column is false under the three-valued logic
|
|
3048
|
+
* {@link workflowFilterMatches} implements, *including the inverses* — so the
|
|
3049
|
+
* load comes out empty and every node reports success. A rename of a column that
|
|
3050
|
+
* cannot exist renames nothing, so the target is absent, and a sink writing it
|
|
3051
|
+
* commits NULL into every row. That is the exact shape `property-names.ts` was
|
|
3052
|
+
* written about, one node upstream of where it can be caught.
|
|
3053
|
+
*/
|
|
3054
|
+
function checkColumnsProduced(graph, issues) {
|
|
3055
|
+
for (const node of graph.nodes ?? []) {
|
|
3056
|
+
// Narrowed off the union rather than tested with a property check, so a kind
|
|
3057
|
+
// that starts naming columns without being answered for here is a type error
|
|
3058
|
+
// at `missingColumnMessage` and not a check that silently passes.
|
|
3059
|
+
if (node.kind !== 'filter' && node.kind !== 'rename')
|
|
3060
|
+
continue;
|
|
3061
|
+
const named = node.kind === 'filter'
|
|
3062
|
+
? workflowFilterColumns(node.predicate)
|
|
3063
|
+
: Object.keys(node.columns ?? {});
|
|
3064
|
+
if (named.length === 0)
|
|
3065
|
+
continue;
|
|
3066
|
+
const known = workflowKnownColumns(graph, node.id);
|
|
3067
|
+
if (known === undefined)
|
|
3068
|
+
continue;
|
|
3069
|
+
const missing = named.filter((column) => column.length > 0 && !known.has(column));
|
|
3070
|
+
if (missing.length === 0)
|
|
3071
|
+
continue;
|
|
3072
|
+
issues.push({
|
|
3073
|
+
code: 'column-not-produced',
|
|
3074
|
+
nodeIds: [node.id],
|
|
3075
|
+
message: missingColumnMessage(node, missing, known),
|
|
3076
|
+
});
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
/** The sentence {@link checkColumnsProduced} says, per kind. */
|
|
3080
|
+
function missingColumnMessage(node, missing, known) {
|
|
3081
|
+
const quoted = (names) => [...names].map((column) => JSON.stringify(column)).join(', ');
|
|
3082
|
+
const consequence = node.kind === 'filter'
|
|
3083
|
+
? 'A test on a column that is not there matches no row — not even a "does not equal" test — so this load would come out empty and every node would report success.'
|
|
3084
|
+
: 'A rename of a column that is not there does nothing, so the column it was meant to produce is absent and a sink writing it commits NULL into every row.';
|
|
3085
|
+
return `${node.kind === 'filter' ? 'Filter' : 'Rename'} "${node.name}" (${node.id}) names ${quoted(missing)}, and nothing upstream produces ${missing.length === 1 ? 'that column' : 'those columns'}. A rename above this node drops every column it does not name, so what reaches here is exactly ${quoted(known)}. ${consequence}`;
|
|
3086
|
+
}
|
|
2555
3087
|
function sortedEntries(config) {
|
|
2556
3088
|
return Object.keys(config)
|
|
2557
3089
|
.sort()
|
|
@@ -2607,6 +3139,8 @@ function isWorkflowNode(value) {
|
|
|
2607
3139
|
? isWorkflowFilterPredicate(Reflect.get(value, 'predicate'))
|
|
2608
3140
|
: false;
|
|
2609
3141
|
}
|
|
3142
|
+
if (kind === 'rename')
|
|
3143
|
+
return isRenameNodeShape(value);
|
|
2610
3144
|
if (kind === 'source') {
|
|
2611
3145
|
const sourceKind = Reflect.get(value, 'sourceKind');
|
|
2612
3146
|
const config = Reflect.get(value, 'config');
|
|
@@ -2614,6 +3148,22 @@ function isWorkflowNode(value) {
|
|
|
2614
3148
|
}
|
|
2615
3149
|
return isWorkflowNodeKindUnhandled(kind);
|
|
2616
3150
|
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Everything a `rename` node carries.
|
|
3153
|
+
*
|
|
3154
|
+
* `unnamed` absent is accepted and always will be — it is what every rename
|
|
3155
|
+
* written before the field existed carries, and it means `keep`. A value that is
|
|
3156
|
+
* present and unrecognised is refused rather than defaulted, for the reason an
|
|
3157
|
+
* unrecognised `edge.branch` is: reading it back as `keep` would turn a
|
|
3158
|
+
* projection into a pass-through silently, and the sink would commit every
|
|
3159
|
+
* column the author meant to remove.
|
|
3160
|
+
*/
|
|
3161
|
+
function isRenameNodeShape(value) {
|
|
3162
|
+
const unnamed = Reflect.get(value, 'unnamed');
|
|
3163
|
+
if (unnamed !== undefined && !isWorkflowRenameUnnamed(unnamed))
|
|
3164
|
+
return false;
|
|
3165
|
+
return isWorkflowRenameColumns(Reflect.get(value, 'columns'));
|
|
3166
|
+
}
|
|
2617
3167
|
/**
|
|
2618
3168
|
* Everything a `call` node carries, checked as strictly as a source's.
|
|
2619
3169
|
*
|
|
@@ -2831,6 +3381,25 @@ function supportsReusableNodes(store) {
|
|
|
2831
3381
|
function supportsWorkflowStages(store) {
|
|
2832
3382
|
return typeof store.writeStage === 'function' && typeof store.readStage === 'function';
|
|
2833
3383
|
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Whether this store will hand a staged batch over without decoding it.
|
|
3386
|
+
*
|
|
3387
|
+
* Both methods, never one: a rename that could read the payload and not write
|
|
3388
|
+
* one back would have to decode its own output to store it, which is the rebuild
|
|
3389
|
+
* the pair exists to avoid. The methods rather than a flag, the same argument
|
|
3390
|
+
* {@link supportsWorkflows} makes.
|
|
3391
|
+
*
|
|
3392
|
+
* A store that answers no is not broken and nothing degrades except speed — the
|
|
3393
|
+
* rename node falls back to `readStage`/`writeStage` and produces identical
|
|
3394
|
+
* rows. Which path ran is said in the run log, because "this rename was
|
|
3395
|
+
* metadata-only" is a claim, and a claim that could quietly stop being true is
|
|
3396
|
+
* worse than no claim.
|
|
3397
|
+
*/
|
|
3398
|
+
function supportsStagePayloads(store) {
|
|
3399
|
+
return (supportsWorkflowStages(store) &&
|
|
3400
|
+
typeof store.readStagePayload === 'function' &&
|
|
3401
|
+
typeof store.writeStagePayload === 'function');
|
|
3402
|
+
}
|
|
2834
3403
|
/**
|
|
2835
3404
|
* Whether an operator can set a load expectation on this deployment at all.
|
|
2836
3405
|
*
|