@grepr/cli 1.6.30-551accf → 1.6.42-91f3e80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/build/dist/commands/backfill-command.d.ts +17 -0
  2. package/build/dist/commands/backfill-command.d.ts.map +1 -0
  3. package/build/dist/commands/backfill-command.js +115 -0
  4. package/build/dist/commands/backfill-command.js.map +1 -0
  5. package/build/dist/commands/job-draft-command.js +5 -6
  6. package/build/dist/commands/job-draft-command.js.map +1 -1
  7. package/build/dist/commands/query-command.d.ts +4 -18
  8. package/build/dist/commands/query-command.d.ts.map +1 -1
  9. package/build/dist/commands/query-command.js +5 -49
  10. package/build/dist/commands/query-command.js.map +1 -1
  11. package/build/dist/commands/sql-validate-command.d.ts +7 -0
  12. package/build/dist/commands/sql-validate-command.d.ts.map +1 -0
  13. package/build/dist/commands/sql-validate-command.js +29 -0
  14. package/build/dist/commands/sql-validate-command.js.map +1 -0
  15. package/build/dist/grepr.d.ts +1 -1
  16. package/build/dist/grepr.d.ts.map +1 -1
  17. package/build/dist/grepr.js +6 -0
  18. package/build/dist/grepr.js.map +1 -1
  19. package/build/dist/lib/backfill-grepr-link.d.ts +9 -0
  20. package/build/dist/lib/backfill-grepr-link.d.ts.map +1 -0
  21. package/build/dist/lib/backfill-grepr-link.js +31 -0
  22. package/build/dist/lib/backfill-grepr-link.js.map +1 -0
  23. package/build/dist/lib/backfill-vendor-links.d.ts +15 -0
  24. package/build/dist/lib/backfill-vendor-links.d.ts.map +1 -0
  25. package/build/dist/lib/backfill-vendor-links.js +181 -0
  26. package/build/dist/lib/backfill-vendor-links.js.map +1 -0
  27. package/build/dist/lib/backfill-vendors.d.ts +45 -0
  28. package/build/dist/lib/backfill-vendors.d.ts.map +1 -0
  29. package/build/dist/lib/backfill-vendors.js +43 -0
  30. package/build/dist/lib/backfill-vendors.js.map +1 -0
  31. package/build/dist/lib/backfill.d.ts +51 -0
  32. package/build/dist/lib/backfill.d.ts.map +1 -0
  33. package/build/dist/lib/backfill.js +343 -0
  34. package/build/dist/lib/backfill.js.map +1 -0
  35. package/build/dist/lib/grepr-api-client.d.ts +2 -1
  36. package/build/dist/lib/grepr-api-client.d.ts.map +1 -1
  37. package/build/dist/lib/grepr-api-client.js +13 -0
  38. package/build/dist/lib/grepr-api-client.js.map +1 -1
  39. package/build/dist/lib/job-patch.d.ts +34 -2
  40. package/build/dist/lib/job-patch.d.ts.map +1 -1
  41. package/build/dist/lib/job-patch.js +257 -60
  42. package/build/dist/lib/job-patch.js.map +1 -1
  43. package/build/dist/lib/job-plan.d.ts.map +1 -1
  44. package/build/dist/lib/job-plan.js +8 -8
  45. package/build/dist/lib/job-plan.js.map +1 -1
  46. package/build/dist/lib/query-predicate.d.ts +28 -0
  47. package/build/dist/lib/query-predicate.d.ts.map +1 -0
  48. package/build/dist/lib/query-predicate.js +66 -0
  49. package/build/dist/lib/query-predicate.js.map +1 -0
  50. package/build/dist/lib/time-utils.d.ts +25 -0
  51. package/build/dist/lib/time-utils.d.ts.map +1 -1
  52. package/build/dist/lib/time-utils.js +71 -0
  53. package/build/dist/lib/time-utils.js.map +1 -1
  54. package/build/dist/lib/transformers-embeddings.d.ts.map +1 -1
  55. package/build/dist/lib/transformers-embeddings.js +14 -1
  56. package/build/dist/lib/transformers-embeddings.js.map +1 -1
  57. package/build/dist/openapi/openApiTypes.d.ts +438 -18
  58. package/build/dist/openapi/openApiTypes.d.ts.map +1 -1
  59. package/build/dist/openapi/openApiTypes.js +19 -1
  60. package/build/dist/openapi/openApiTypes.js.map +1 -1
  61. package/build/dist/types.d.ts +3 -7
  62. package/build/dist/types.d.ts.map +1 -1
  63. package/build/dist/types.js +1 -1
  64. package/build/dist/types.js.map +1 -1
  65. package/package.json +15 -16
@@ -1,7 +1,12 @@
1
1
  /** Structured patch format for grepr pipelines: a list of ops applied locally before any production write (see {@link JobBackend} for the two substrates). */
2
- import { LogAttributesRemapperType, LogReducerType, GrokParserType, LogsFilterType, ConditionNodeKind, DropNodeKind, PassthroughNodeKind, DatadogQueryPredicateType, DatadogLogSinkType, SplunkLogSinkType, NewRelicLogSinkType, SumoLogSinkType, OtlpLogSinkType, LogsIcebergTableSinkType, TemplateOperationType, TemplateQueryExceptionType, SumAttributesMergeStrategyType, MinAttributesMergeStrategyType, MaxAttributesMergeStrategyType, AverageAttributesMergeStrategyType, } from '../openapi/openApiTypes.js';
2
+ import { ConditionNodeKind, DropNodeKind, PassthroughNodeKind, SqlNodeKind, SqlNodeOutputRouting, SqlOperationType, SqlOutputStatementType, LogAttributesRemapperType, LogReducerType, GrokParserType, LogsFilterType, DatadogQueryPredicateType, DatadogLogSinkType, SplunkLogSinkType, NewRelicLogSinkType, SumoLogSinkType, OtlpLogSinkType, LogsIcebergTableSinkType, TemplateOperationType, TemplateQueryExceptionType, SumAttributesMergeStrategyType, MinAttributesMergeStrategyType, MaxAttributesMergeStrategyType, AverageAttributesMergeStrategyType, } from '../openapi/openApiTypes.js';
3
3
  import { parseEdge } from './job-graph-utils.js';
4
4
  import { RAW_ATTRIBUTES_REMAPPER, RAW_ATTRIBUTES_REMAPPER_TYPE, RAW_JSON_PROCESSOR, RAW_JSON_PROCESSOR_TYPE, RAW_LOG_REDUCER, RAW_PARSER_TYPES, RAW_PRE_EXCEPTIONS_FILTER, RAW_PRE_PARSER_FILTER, RAW_PRE_WAREHOUSE_FILTER, } from './job-graph-log-pipeline-constants.js';
5
+ /** The accepted-phases suffix shared by every template transform-phase error. */
6
+ const ACCEPTED_TRANSFORM_PHASES_MESSAGE = 'Accepted phases are pre-parser, pre-warehouse, and pre-exceptions.';
7
+ /** The downstream pipeline steps a `sql_output` may route to (the generated {@link SqlNodeOutputRouting} values). */
8
+ const SQL_OUTPUT_ROUTING_TARGETS = new Set(Object.values(SqlNodeOutputRouting));
9
+ const SQL_TRANSFORM_MAIN_STREAMS = new Set(['drop', 'passthrough']);
5
10
  /** Vendor log sink types the UI supports as `add-sink target: 'vendor'`. */
6
11
  const VENDOR_LOG_SINK_TYPES = new Set([
7
12
  DatadogLogSinkType.datadog_log_sink,
@@ -33,6 +38,9 @@ const REQUIRED_OP_FIELDS = {
33
38
  'remove-parser': [['name', 'string']],
34
39
  'set-filter': [['phase', 'string'], ['filter', 'object']],
35
40
  'clear-filter': [['phase', 'string']],
41
+ 'set-transform-chain': [['phase', 'string'], ['root', 'object']],
42
+ 'clear-transform-chain': [['phase', 'string']],
43
+ 'set-sql-transform': [['phase', 'string'], ['sqlOperation', 'object'], ['outputRouting', 'object'], ['mainStream', 'string']],
36
44
  'add-source': [['source', 'object']],
37
45
  'remove-source': [['name', 'string']],
38
46
  // Target-conditional fields (vendor removal's `name`, sink shape) stay with the apply-time guards.
@@ -204,6 +212,12 @@ function applyOperation(input, op, index) {
204
212
  return applySetFilter(input, op.phase, op.filter, index);
205
213
  case 'clear-filter':
206
214
  return applyClearFilter(input, op.phase, index);
215
+ case 'set-transform-chain':
216
+ return applySetTransformChain(input, op.phase, op.root, index);
217
+ case 'clear-transform-chain':
218
+ return applyClearTransformChain(input, op.phase, index);
219
+ case 'set-sql-transform':
220
+ return applySetSqlTransform(input, op.phase, op.sqlOperation, op.outputRouting, op.mainStream, op.gate, index);
207
221
  case 'add-source':
208
222
  return applyAddSource(input, op.source, index);
209
223
  case 'remove-source':
@@ -271,6 +285,13 @@ function applyJobGraphOperation(jobGraph, op, index) {
271
285
  return jobGraphSetFilter(jobGraph, op.phase, op.filter, index);
272
286
  case 'clear-filter':
273
287
  return jobGraphClearFilter(jobGraph, op.phase, index);
288
+ case 'set-sql-transform':
289
+ throw new Error(`Operation ${index} (set-sql-transform): SQL transform edits are only supported for ` +
290
+ `template-backed log-reducer jobs in the CLI. This job is a direct job graph. `);
291
+ case 'set-transform-chain':
292
+ case 'clear-transform-chain':
293
+ throw new Error(`Operation ${index} (${op.op}): transform-chain edits are only supported for ` +
294
+ `template-backed log-reducer jobs in the CLI. This job is a direct job graph. `);
274
295
  case 'add-source':
275
296
  return jobGraphAddSource(jobGraph, op.source, index);
276
297
  case 'remove-source':
@@ -849,9 +870,9 @@ function rawFilterNameForPhase(phase, index, opLabel) {
849
870
  return RAW_PRE_WAREHOUSE_FILTER;
850
871
  case 'pre-exceptions':
851
872
  return RAW_PRE_EXCEPTIONS_FILTER;
852
- case 'pre-aggregation':
853
- throw new Error(`Operation ${index} (${opLabel}): phase "pre-aggregation" has no canonical UI raw-graph stage. ` +
854
- `Use "pre-warehouse" or "pre-exceptions" for raw UI log graphs.`);
873
+ default:
874
+ throw new Error(`Operation ${index} (${opLabel}): phase "${phase}" has no canonical UI raw-graph stage. ` +
875
+ ACCEPTED_TRANSFORM_PHASES_MESSAGE);
855
876
  }
856
877
  }
857
878
  function assertRawUiLogGraph(jobGraph, opLabel, index, requiredNames) {
@@ -1097,79 +1118,254 @@ function applyRemoveParser(input, name, index) {
1097
1118
  input.parsers.splice(idx, 1);
1098
1119
  }
1099
1120
  /**
1100
- * Maps a `FilterPhase` (the op's hyphenated wire value, a key of the legacy
1101
- * `LogReducerFilters`) to the corresponding `Transforms` slot. `pre-aggregation`
1102
- * has no transforms slot and maps to `null`.
1121
+ * Map a filter phase to its template `transforms` slot key. Only the three
1122
+ * transform-backed phases are accepted; any other phase fails with the shared
1123
+ * accepted-phases error.
1103
1124
  */
1104
- const PHASE_TO_TRANSFORM_KEY = {
1105
- 'pre-parser': 'preParser',
1106
- 'pre-warehouse': 'preWarehouse',
1107
- 'pre-exceptions': 'preExceptions',
1108
- 'pre-aggregation': null,
1109
- };
1125
+ function phaseToTransformSlot(phase, index, opLabel) {
1126
+ switch (phase) {
1127
+ case 'pre-parser':
1128
+ return 'preParser';
1129
+ case 'pre-warehouse':
1130
+ return 'preWarehouse';
1131
+ case 'pre-exceptions':
1132
+ return 'preExceptions';
1133
+ default:
1134
+ throw new Error(`Operation ${index} (${opLabel}): phase "${phase}" is not accepted. ${ACCEPTED_TRANSFORM_PHASES_MESSAGE}`);
1135
+ }
1136
+ }
1137
+ function makePassthroughNode() {
1138
+ return { kind: PassthroughNodeKind.passthrough_node };
1139
+ }
1140
+ function makeDropNode() {
1141
+ return { kind: DropNodeKind.drop_node };
1142
+ }
1143
+ function makeMainStreamNode(mainStream) {
1144
+ return mainStream === 'drop' ? makeDropNode() : makePassthroughNode();
1145
+ }
1146
+ function nodeKind(node) {
1147
+ return node?.kind;
1148
+ }
1149
+ export function isDropNode(node) {
1150
+ return nodeKind(node) === DropNodeKind.drop_node;
1151
+ }
1152
+ export function isPassthroughNode(node) {
1153
+ return nodeKind(node) === PassthroughNodeKind.passthrough_node;
1154
+ }
1155
+ export function isSqlNode(node) {
1156
+ return nodeKind(node) === SqlNodeKind.sql_node;
1157
+ }
1158
+ export function isConditionNode(node) {
1159
+ return nodeKind(node) === ConditionNodeKind.condition_node;
1160
+ }
1110
1161
  /**
1111
- * Resolves the `Transforms` slot for a phase, throwing for `pre-aggregation`
1112
- * (which has no chain slot in the transforms model).
1113
- *
1114
- * @param phase the filter phase from the op
1115
- * @param index the operation index, for error messages
1116
- * @param opLabel the op name, for error messages
1117
- * @returns the matching `keyof SchemaTransforms`
1118
- * @throws Error when the phase has no transforms slot
1162
+ * Build a CLI-managed filter gate routing kept events into `passSide` and
1163
+ * dropping the rest. A normal keep filter passes `thenAction`; an inverted
1164
+ * filter swaps the branches. `maxLateEventTimestampDelta` becomes the gate's
1165
+ * root `lateDataDuration`, matching the previous `set-filter` behavior.
1119
1166
  */
1120
- function transformKeyForPhase(phase, index, opLabel) {
1121
- const key = PHASE_TO_TRANSFORM_KEY[phase];
1122
- if (!key) {
1123
- throw new Error(`Operation ${index} (${opLabel}): phase "${phase}" has no transforms slot. ` +
1124
- `Use "pre-parser", "pre-warehouse", or "pre-exceptions".`);
1167
+ function makeFilterGate(filter, passSide) {
1168
+ const drop = makeDropNode();
1169
+ const gate = {
1170
+ kind: ConditionNodeKind.condition_node,
1171
+ predicate: filter.predicate,
1172
+ thenAction: filter.inverted ? drop : passSide,
1173
+ elseAction: filter.inverted ? passSide : drop,
1174
+ };
1175
+ if (filter.maxLateEventTimestampDelta !== undefined) {
1176
+ gate.lateDataDuration = filter.maxLateEventTimestampDelta;
1125
1177
  }
1126
- return key;
1127
- }
1128
- /** True when the existing slot value is a keep-style filter (then keep / else drop), i.e. not inverted. */
1129
- function isInvertedConditionNode(node) {
1130
- return (node.thenAction?.kind === DropNodeKind.drop_node &&
1131
- node.elseAction?.kind === PassthroughNodeKind.passthrough_node);
1178
+ return gate;
1132
1179
  }
1133
1180
  /**
1134
- * Compiles a keep-style {@link SchemaLogsFilter} into the equivalent single
1135
- * {@link SchemaConditionNode} chain: matching logs flow through, non-matching
1136
- * are dropped (arms swap when the filter is `inverted`). Carries over the two
1137
- * documented merge fields (`inverted`, `maxLateEventTimestampDelta`) from an
1138
- * existing condition node when the incoming filter omits them, preserving
1139
- * `set-filter`'s merge contract.
1140
- *
1141
- * @param filter the incoming logs-filter (predicate + optional inverted/lateness)
1142
- * @param existing the current condition node in the slot, if any, for field carry-over
1143
- * @returns a condition node that compiles to the same logs-filter vertex
1181
+ * Detect whether `root` is an unambiguous CLI-managed filter gate: a
1182
+ * `ConditionNode` with exactly one terminal `DropNode` branch. Returns the pass
1183
+ * side and inversion, or `undefined` when `root` is not a condition or has
1184
+ * meaningful (non-drop) work on both branches — a chain the CLI must not guess
1185
+ * at.
1144
1186
  */
1145
- function filterToConditionNode(filter, existing) {
1146
- const keep = { kind: PassthroughNodeKind.passthrough_node };
1147
- const drop = { kind: DropNodeKind.drop_node };
1148
- const inverted = filter.inverted ?? (existing ? isInvertedConditionNode(existing) : false);
1149
- const lateDataDuration = filter.maxLateEventTimestampDelta ?? existing?.lateDataDuration;
1187
+ function findSimpleRootFilterGate(root) {
1188
+ if (!isConditionNode(root))
1189
+ return undefined;
1190
+ const cond = root;
1191
+ const thenIsDrop = isDropNode(cond.thenAction);
1192
+ const elseIsDrop = isDropNode(cond.elseAction);
1193
+ // Exactly one branch must be a drop; both-drop or neither-drop is ambiguous.
1194
+ if (thenIsDrop === elseIsDrop)
1195
+ return undefined;
1150
1196
  return {
1151
- kind: ConditionNodeKind.condition_node,
1152
- predicate: filter.predicate ?? existing?.predicate ?? { type: DatadogQueryPredicateType.datadog_query, query: '' },
1153
- thenAction: inverted ? drop : keep,
1154
- elseAction: inverted ? keep : drop,
1155
- ...(lateDataDuration ? { lateDataDuration } : {}),
1197
+ passSide: elseIsDrop ? cond.thenAction : cond.elseAction,
1198
+ inverted: !elseIsDrop,
1199
+ lateDataDuration: cond.lateDataDuration,
1156
1200
  };
1157
1201
  }
1202
+ /**
1203
+ * Install or update a filter gate at the phase's transform slot:
1204
+ * - empty slot: `IF filter THEN PASSTHROUGH ELSE DROP`
1205
+ * - existing simple gate: rebuild with the new predicate, keeping the pass side
1206
+ * and inheriting `inverted`/`lateDataDuration` when the patch omits them (raw
1207
+ * merge semantics — a predicate-only edit must not flip keep↔drop)
1208
+ * - any other existing chain (e.g. a SqlNode): `IF filter THEN <chain> ELSE DROP`
1209
+ */
1158
1210
  function applySetFilter(input, phase, filter, index) {
1159
1211
  if (!filter || typeof filter !== 'object') {
1160
1212
  throw new Error(`Operation ${index} (set-filter): filter must be an object`);
1161
1213
  }
1162
- const key = transformKeyForPhase(phase, index, 'set-filter');
1163
- const existing = input.transforms?.[key];
1164
- const node = filterToConditionNode(filter, existing?.kind === ConditionNodeKind.condition_node ? existing : undefined);
1165
- input.transforms = { ...(input.transforms ?? {}), [key]: node };
1214
+ if (!isEventPredicate(filter.predicate)) {
1215
+ throw new Error(`Operation ${index} (set-filter): filter.predicate must be an EventPredicate object with a "type"`);
1216
+ }
1217
+ const slot = phaseToTransformSlot(phase, index, 'set-filter');
1218
+ const transforms = input.transforms ?? {};
1219
+ const existing = transforms[slot];
1220
+ const match = existing === undefined ? undefined : findSimpleRootFilterGate(existing);
1221
+ const passSide = existing === undefined ? makePassthroughNode() : match?.passSide ?? existing;
1222
+ const gateFilter = match
1223
+ ? {
1224
+ ...filter,
1225
+ inverted: filter.inverted ?? match.inverted,
1226
+ maxLateEventTimestampDelta: filter.maxLateEventTimestampDelta ?? match.lateDataDuration,
1227
+ }
1228
+ : filter;
1229
+ transforms[slot] = makeFilterGate(gateFilter, passSide);
1230
+ input.transforms = transforms;
1166
1231
  }
1232
+ /**
1233
+ * Remove a root filter gate from the phase's transform slot, replacing the slot
1234
+ * with the gate's pass side. No-ops on an empty slot. Fails clearly when the
1235
+ * slot holds a chain that is not an unambiguous filter gate, rather than
1236
+ * guessing which branch is the filter.
1237
+ */
1167
1238
  function applyClearFilter(input, phase, index) {
1168
- const key = transformKeyForPhase(phase, index, 'clear-filter');
1169
- if (!input.transforms?.[key])
1239
+ const slot = phaseToTransformSlot(phase, index, 'clear-filter');
1240
+ const transforms = input.transforms;
1241
+ const existing = transforms?.[slot];
1242
+ if (existing === undefined)
1170
1243
  return;
1171
- const { [key]: _removed, ...rest } = input.transforms;
1172
- input.transforms = rest;
1244
+ const match = findSimpleRootFilterGate(existing);
1245
+ if (!match) {
1246
+ const detail = isConditionNode(existing)
1247
+ ? 'contains a branching transform chain, not a simple filter gate'
1248
+ : `contains a ${nodeKind(existing) ?? 'non-condition'} at its root, not a filter gate`;
1249
+ throw new Error(`Operation ${index} (clear-filter): template transform slot "${slot}" ${detail}. ` +
1250
+ `Use set-transform-chain to edit it explicitly.`);
1251
+ }
1252
+ transforms[slot] = match.passSide;
1253
+ input.transforms = transforms;
1254
+ }
1255
+ /** True when `value` looks like an `EventPredicate` (an object carrying a string `type` discriminator). */
1256
+ function isEventPredicate(value) {
1257
+ return isRecord(value) && typeof value['type'] === 'string';
1258
+ }
1259
+ /**
1260
+ * Replace the phase's transform slot with a hand-authored chain root. The CLI
1261
+ * does not introspect or validate the tree beyond requiring an object; the
1262
+ * template compiler remains the final authority.
1263
+ */
1264
+ function applySetTransformChain(input, phase, root, index) {
1265
+ if (!isRecord(root)) {
1266
+ throw new Error(`Operation ${index} (set-transform-chain): root must be a chain-node object`);
1267
+ }
1268
+ const slot = phaseToTransformSlot(phase, index, 'set-transform-chain');
1269
+ const transforms = input.transforms ?? {};
1270
+ transforms[slot] = root;
1271
+ input.transforms = transforms;
1272
+ }
1273
+ /** Remove a phase's entire transform slot (distinct from `clear-filter`, which removes only a filter gate). */
1274
+ function applyClearTransformChain(input, phase, index) {
1275
+ const slot = phaseToTransformSlot(phase, index, 'clear-transform-chain');
1276
+ if (input.transforms) {
1277
+ Reflect.deleteProperty(input.transforms, slot);
1278
+ }
1279
+ }
1280
+ /**
1281
+ * Replace the phase's transform slot with a generated `SqlNode` chain. Any
1282
+ * existing chain is replaced — callers needing to preserve or merge a chain
1283
+ * must use `set-transform-chain`.
1284
+ */
1285
+ function applySetSqlTransform(input, phase, sqlOperation, outputRouting, mainStream, gate, index) {
1286
+ const slot = phaseToTransformSlot(phase, index, 'set-sql-transform');
1287
+ validateSqlTransform(sqlOperation, outputRouting, mainStream, gate, index);
1288
+ const sqlChain = {
1289
+ kind: SqlNodeKind.sql_node,
1290
+ sqlOperation,
1291
+ outputRouting,
1292
+ next: makeMainStreamNode(mainStream),
1293
+ };
1294
+ // An ungated drop (`mainStream: 'drop'`, no gate) still needs a condition node
1295
+ // wrapper with a match-all (`*`) predicate. The `elseAction` passthrough is
1296
+ // unreachable under `*`, but it is load-bearing, NOT dead code: it is the SQL
1297
+ // node's connected main output edge. A bare `sql-node` whose `next` is a drop
1298
+ // compiles to a disconnected `chainOut` in the Flink graph (see ENGT-4852 fix);
1299
+ // the passthrough else-branch is what keeps that main output connected. Do not
1300
+ // "simplify" it away — `test_setSqlTransform_noGate_dropMainStream_buildsMatchAllConditionNode`
1301
+ // pins this shape.
1302
+ const effectiveGate = gate ?? { type: DatadogQueryPredicateType.datadog_query, query: '*' };
1303
+ const root = mainStream === 'passthrough' && gate === undefined
1304
+ ? sqlChain
1305
+ : {
1306
+ kind: ConditionNodeKind.condition_node,
1307
+ predicate: effectiveGate,
1308
+ thenAction: sqlChain,
1309
+ elseAction: makePassthroughNode(),
1310
+ };
1311
+ const transforms = input.transforms ?? {};
1312
+ transforms[slot] = root;
1313
+ input.transforms = transforms;
1314
+ }
1315
+ /**
1316
+ * Validate a `set-sql-transform` op before mutating the input, so malformed
1317
+ * SQL fails with a readable CLI error ahead of `job:draft`.
1318
+ */
1319
+ function validateSqlTransform(sqlOperation, outputRouting, mainStream, gate, index) {
1320
+ const fail = (detail) => {
1321
+ throw new Error(`Operation ${index} (set-sql-transform): ${detail}`);
1322
+ };
1323
+ if (!isRecord(sqlOperation) || sqlOperation['type'] !== SqlOperationType.sql_operation) {
1324
+ fail(`sqlOperation must be an object with type "${SqlOperationType.sql_operation}"`);
1325
+ }
1326
+ const statements = sqlOperation.statements;
1327
+ if (!Array.isArray(statements) || statements.length === 0) {
1328
+ fail('sqlOperation.statements must be a non-empty array');
1329
+ }
1330
+ const outputNames = statements
1331
+ .filter(s => s.type === SqlOutputStatementType.sql_output)
1332
+ .map(s => s.outputName);
1333
+ if (outputNames.length === 0) {
1334
+ fail('sqlOperation.statements must include at least one sql_output statement');
1335
+ }
1336
+ const seen = new Set();
1337
+ for (const name of outputNames) {
1338
+ if (typeof name !== 'string' || name.length === 0) {
1339
+ fail('every sql_output statement must have a non-empty outputName');
1340
+ }
1341
+ if (seen.has(name)) {
1342
+ fail(`duplicate sql_output outputName "${name}"; output names must be unique`);
1343
+ }
1344
+ seen.add(name);
1345
+ }
1346
+ if (!isRecord(outputRouting)) {
1347
+ fail('outputRouting must be an object');
1348
+ }
1349
+ if (typeof mainStream !== 'string' || !SQL_TRANSFORM_MAIN_STREAMS.has(mainStream)) {
1350
+ fail(`mainStream must be one of: ${[...SQL_TRANSFORM_MAIN_STREAMS].join(', ')}`);
1351
+ }
1352
+ for (const [key, target] of Object.entries(outputRouting)) {
1353
+ if (!seen.has(key)) {
1354
+ fail(`outputRouting key "${key}" does not match any sql_output outputName`);
1355
+ }
1356
+ if (typeof target !== 'string' || !SQL_OUTPUT_ROUTING_TARGETS.has(target)) {
1357
+ fail(`outputRouting["${key}"] = ${JSON.stringify(target)} is not a valid target step. ` +
1358
+ `Valid targets: ${[...SQL_OUTPUT_ROUTING_TARGETS].join(', ')}.`);
1359
+ }
1360
+ }
1361
+ for (const name of seen) {
1362
+ if (!Object.prototype.hasOwnProperty.call(outputRouting, name)) {
1363
+ fail(`sql_output "${name}" has no route in outputRouting`);
1364
+ }
1365
+ }
1366
+ if (gate !== undefined && !isEventPredicate(gate)) {
1367
+ fail('gate must be an EventPredicate object with a "type"');
1368
+ }
1173
1369
  }
1174
1370
  function applyAddSource(input, source, index) {
1175
1371
  assertOperationIdentity(source, 'add-source', index, 'source');
@@ -1402,6 +1598,7 @@ const INPUT_FIELD_TOUCHES = {
1402
1598
  rawSinkConfig: 'sink',
1403
1599
  datasetId: 'sink',
1404
1600
  conditionalDatasets: 'sink',
1601
+ confirmedDeliveryEnabled: 'sink',
1405
1602
  reducer: 'transform',
1406
1603
  parsers: 'transform',
1407
1604
  transforms: 'transform',