@octaviaflow/flow-rules 0.17.0 → 0.19.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/index.cjs CHANGED
@@ -45,7 +45,6 @@ __export(exports_src, {
45
45
  ActionStatus: () => ActionStatus,
46
46
  ActionType: () => ActionType,
47
47
  AggregateConfig: () => AggregateConfig,
48
- AggregateFunction: () => AggregateFunction,
49
48
  AiAgentConfig: () => AiAgentConfig,
50
49
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
51
50
  CONFIG_CONTRACTS: () => CONFIG_CONTRACTS,
@@ -67,6 +66,8 @@ __export(exports_src, {
67
66
  FILE_EDIT_SCHEMA: () => FILE_EDIT_SCHEMA,
68
67
  FILE_EDIT_SOURCES: () => FILE_EDIT_SOURCES,
69
68
  FILE_FORMAT_OPERATIONS: () => FILE_FORMAT_OPERATIONS,
69
+ FILE_INPUT_HELP: () => FILE_INPUT_HELP,
70
+ FILE_OUTPUT_HELP: () => FILE_OUTPUT_HELP,
70
71
  FTP_OPERATIONS: () => FTP_OPERATIONS,
71
72
  FieldType: () => FieldType,
72
73
  ForEachConfig: () => ForEachConfig,
@@ -76,22 +77,33 @@ __export(exports_src, {
76
77
  GroupConfig: () => GroupConfig,
77
78
  IDP_EXTRACT_STRATEGIES: () => IDP_EXTRACT_STRATEGIES,
78
79
  IdpExtractConfig: () => IdpExtractConfig,
79
- JoinType: () => JoinType,
80
80
  JsonActionConfig: () => JsonActionConfig,
81
- LogicalOperator: () => LogicalOperator,
82
81
  MAPPING_FUNCTIONS: () => MAPPING_FUNCTIONS,
83
82
  MAPPING_FUNCTION_SPECS: () => MAPPING_FUNCTION_SPECS,
83
+ MAX_CONDITION_DEPTH: () => MAX_CONDITION_DEPTH,
84
84
  MAX_STEP_CONFIG_BYTES: () => MAX_STEP_CONFIG_BYTES,
85
85
  ManualTriggerConfig: () => ManualTriggerConfig,
86
86
  NodeStatus: () => NodeStatus,
87
87
  ONEDRIVE_OPERATIONS: () => ONEDRIVE_OPERATIONS,
88
88
  OneDriveActionConfig: () => OneDriveActionConfig,
89
89
  PLATFORM_CONFIG_KEYS: () => PLATFORM_CONFIG_KEYS,
90
+ QUERY_AGGREGATE_FUNCTIONS: () => QUERY_AGGREGATE_FUNCTIONS,
91
+ QUERY_AUTHORED_SQL_MESSAGE: () => QUERY_AUTHORED_SQL_MESSAGE,
92
+ QUERY_COMPARISON_OPERATORS: () => QUERY_COMPARISON_OPERATORS,
93
+ QUERY_CONDITION_OPERATORS: () => QUERY_CONDITION_OPERATORS,
94
+ QUERY_MODES: () => QUERY_MODES,
95
+ QUERY_RELATIVE_DATE_OPERATORS: () => QUERY_RELATIVE_DATE_OPERATORS,
96
+ QUERY_TYPES: () => QUERY_TYPES,
97
+ QUERY_VISUAL_ONLY_MESSAGE: () => QUERY_VISUAL_ONLY_MESSAGE,
90
98
  QueryMode: () => QueryMode,
91
99
  QueryType: () => QueryType,
100
+ RELATIVE_DATE_DIRECTIONS: () => RELATIVE_DATE_DIRECTIONS,
101
+ RELATIVE_DATE_MAX_AMOUNT: () => RELATIVE_DATE_MAX_AMOUNT,
102
+ RELATIVE_DATE_UNITS: () => RELATIVE_DATE_UNITS,
92
103
  S3ActionConfig: () => S3ActionConfig,
93
104
  S3_OPERATIONS: () => S3_OPERATIONS,
94
105
  STORAGE_CONNECTOR_IDS: () => STORAGE_CONNECTOR_IDS,
106
+ STORAGE_DOWNLOAD_OUTPUT_SCHEMA: () => STORAGE_DOWNLOAD_OUTPUT_SCHEMA,
95
107
  SortDirection: () => SortDirection,
96
108
  StorageActionConfig: () => StorageActionConfig,
97
109
  TRANSFORM_MODES: () => TRANSFORM_MODES,
@@ -131,6 +143,7 @@ __export(exports_src, {
131
143
  getStorageProvider: () => getStorageProvider,
132
144
  groupKeyName: () => groupKeyName,
133
145
  inferSchemaFromSample: () => inferSchemaFromSample,
146
+ isConditionGroup: () => isConditionGroup,
134
147
  isEditorPrivateKey: () => isEditorPrivateKey,
135
148
  isEventCapableConnector: () => isEventCapableConnector,
136
149
  isMappingFunction: () => isMappingFunction,
@@ -138,6 +151,8 @@ __export(exports_src, {
138
151
  mappingTransformFunctionName: () => mappingTransformFunctionName,
139
152
  migrateDelayConfig: () => migrateDelayConfig,
140
153
  queryDefinition: () => queryDefinition,
154
+ queryFieldsSatisfied: () => queryFieldsSatisfied,
155
+ queryTablesSatisfied: () => queryTablesSatisfied,
141
156
  resolveRecordSchema: () => resolveRecordSchema,
142
157
  selectsWithoutValues: () => selectsWithoutValues,
143
158
  storageConfigKeys: () => storageConfigKeys,
@@ -146,12 +161,14 @@ __export(exports_src, {
146
161
  transformConnectionToEdge: () => transformConnectionToEdge,
147
162
  transformFlowToWorkflow: () => transformFlowToWorkflow,
148
163
  transformStepToNode: () => transformStepToNode,
164
+ validateBuildQueryConfig: () => validateBuildQueryConfig,
149
165
  validateConnectorTriggerEvents: () => validateConnectorTriggerEvents,
150
166
  validateEdge: () => validateEdge,
151
167
  validateFlowData: () => validateFlowData,
152
168
  validateNode: () => validateNode,
153
169
  validateStepConfig: () => validateStepConfig,
154
170
  validateStepConfigs: () => validateStepConfigs,
171
+ validateVisualConfig: () => validateVisualConfig,
155
172
  validateWorkflow: () => validateWorkflow
156
173
  });
157
174
  module.exports = __toCommonJS(exports_src);
@@ -1180,32 +1197,51 @@ var QueryType;
1180
1197
  QueryType["Delete"] = "delete";
1181
1198
  QueryType["Custom"] = "custom";
1182
1199
  })(QueryType ||= {});
1183
- var LogicalOperator;
1184
- ((LogicalOperator) => {
1185
- LogicalOperator["And"] = "and";
1186
- LogicalOperator["Or"] = "or";
1187
- })(LogicalOperator ||= {});
1200
+ var QUERY_MODES = ["visual" /* Visual */];
1201
+ var QUERY_TYPES = ["select" /* Select */];
1202
+ var QUERY_COMPARISON_OPERATORS = [
1203
+ "=",
1204
+ "!=",
1205
+ ">",
1206
+ "<",
1207
+ ">=",
1208
+ "<=",
1209
+ "equals",
1210
+ "notEquals",
1211
+ "greaterThan",
1212
+ "lessThan",
1213
+ "greaterOrEqual",
1214
+ "lessOrEqual"
1215
+ ];
1216
+ var QUERY_RELATIVE_DATE_OPERATORS = ["inRelative", "notInRelative"];
1217
+ var QUERY_CONDITION_OPERATORS = [
1218
+ ...QUERY_COMPARISON_OPERATORS,
1219
+ "contains",
1220
+ "startsWith",
1221
+ "endsWith",
1222
+ "like",
1223
+ "notLike",
1224
+ "in",
1225
+ "notIn",
1226
+ "between",
1227
+ "isNull",
1228
+ "isNotNull",
1229
+ ...QUERY_RELATIVE_DATE_OPERATORS
1230
+ ];
1231
+ var RELATIVE_DATE_DIRECTIONS = ["last", "next", "this"];
1232
+ var RELATIVE_DATE_UNITS = ["day", "week", "month", "quarter", "year"];
1233
+ var RELATIVE_DATE_MAX_AMOUNT = 3650;
1234
+ var MAX_CONDITION_DEPTH = 3;
1235
+ var isConditionGroup = (c) => !!c && typeof c === "object" && c.type === "group";
1236
+ var QUERY_AGGREGATE_FUNCTIONS = ["COUNT", "COUNT_DISTINCT", "SUM", "AVG", "MIN", "MAX"];
1188
1237
  var SortDirection;
1189
1238
  ((SortDirection) => {
1190
1239
  SortDirection["Ascending"] = "asc";
1191
1240
  SortDirection["Descending"] = "desc";
1192
1241
  })(SortDirection ||= {});
1193
- var JoinType;
1194
- ((JoinType) => {
1195
- JoinType["Inner"] = "inner";
1196
- JoinType["Left"] = "left";
1197
- JoinType["Right"] = "right";
1198
- JoinType["Full"] = "full";
1199
- })(JoinType ||= {});
1200
- var AggregateFunction;
1201
- ((AggregateFunction) => {
1202
- AggregateFunction["Count"] = "count";
1203
- AggregateFunction["Sum"] = "sum";
1204
- AggregateFunction["Average"] = "avg";
1205
- AggregateFunction["Min"] = "min";
1206
- AggregateFunction["Max"] = "max";
1207
- AggregateFunction["CountDistinct"] = "count_distinct";
1208
- })(AggregateFunction ||= {});
1242
+ // src/catalog/isOneOf.ts
1243
+ var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
1244
+
1209
1245
  // src/catalog/general/query/config.ts
1210
1246
  var BuildQueryConfigDefaults = {
1211
1247
  mode: "visual" /* Visual */,
@@ -1215,160 +1251,262 @@ var BuildQueryConfigDefaults = {
1215
1251
  visualConfig: {
1216
1252
  tables: [],
1217
1253
  fields: [],
1218
- filters: undefined,
1254
+ conditions: [],
1219
1255
  joins: [],
1220
1256
  groupBy: [],
1257
+ aggregates: [],
1221
1258
  sort: [],
1222
- limit: 10,
1223
- offset: 0
1224
- },
1225
- sqlConfig: {
1226
- sql: "",
1227
- parameters: {}
1259
+ limit: 10
1228
1260
  }
1229
1261
  };
1230
- var validateBuildQueryConfig = (config) => {
1231
- const errors = [];
1232
- const warnings = [];
1233
- if (!config.connectorId) {
1234
- errors.push({
1235
- field: "connectorId",
1236
- message: "Please select a connector",
1237
- severity: "error"
1238
- });
1262
+ var QUERY_VISUAL_ONLY_MESSAGE = "The Query step is built with the visual builder only — set mode to 'visual' and describe the query in " + "visualConfig (tables, fields, conditions, sort, groupBy, aggregates). Hand-written SQL is not accepted.";
1263
+ var QUERY_AUTHORED_SQL_MESSAGE = "sqlConfig.sql is written by the visual builder, not by hand — remove sqlConfig and describe the query in " + "visualConfig (tables, fields, conditions, sort, groupBy, aggregates) instead.";
1264
+ var err = (field, message) => ({ field, message, severity: "error" });
1265
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1266
+ var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
1267
+ var hasValue = (v) => v !== undefined && v !== null && !(typeof v === "string" && v.trim() === "") && !(Array.isArray(v) && v.length === 0);
1268
+ var arr = (v) => Array.isArray(v) ? v : [];
1269
+ function queryFieldsSatisfied(config) {
1270
+ const vc = isRecord(config?.visualConfig) ? config.visualConfig : undefined;
1271
+ if (!vc)
1272
+ return false;
1273
+ if (vc.selectAll === true)
1274
+ return true;
1275
+ if (arr(vc.fields).length > 0)
1276
+ return true;
1277
+ if (arr(vc.aggregates).length > 0 || arr(vc.groupBy).length > 0)
1278
+ return true;
1279
+ const sql = isRecord(config?.sqlConfig) ? config.sqlConfig.sql : undefined;
1280
+ return arr(vc.tables).length > 0 && nonEmptyString(sql);
1281
+ }
1282
+ function queryTablesSatisfied(config) {
1283
+ const vc = isRecord(config?.visualConfig) ? config.visualConfig : undefined;
1284
+ if (!vc)
1285
+ return false;
1286
+ return vc.selectAll === true || arr(vc.tables).length > 0;
1287
+ }
1288
+ var OPERATOR_LIST = QUERY_CONDITION_OPERATORS.join(", ");
1289
+ function validateRelative(path, relative) {
1290
+ if (!isRecord(relative)) {
1291
+ return [err(`${path}.relative`, `${path}: a relative-date condition needs relative, e.g. { direction: 'last', amount: 30, unit: 'day' }`)];
1239
1292
  }
1240
- if (!config.mode) {
1241
- errors.push({
1242
- field: "mode",
1243
- message: "Query mode is required",
1244
- severity: "error"
1245
- });
1293
+ const issues = [];
1294
+ const { direction, unit, amount, includeCurrent } = relative;
1295
+ if (!isOneOf(RELATIVE_DATE_DIRECTIONS, direction)) {
1296
+ issues.push(err(`${path}.relative.direction`, `${path}.relative.direction must be one of: ${RELATIVE_DATE_DIRECTIONS.join(", ")} (got "${String(direction)}")`));
1246
1297
  }
1247
- if (!config.queryType) {
1248
- errors.push({
1249
- field: "queryType",
1250
- message: "Query type is required",
1251
- severity: "error"
1252
- });
1298
+ if (!isOneOf(RELATIVE_DATE_UNITS, unit)) {
1299
+ issues.push(err(`${path}.relative.unit`, `${path}.relative.unit must be one of: ${RELATIVE_DATE_UNITS.join(", ")} (got "${String(unit)}")`));
1253
1300
  }
1254
- if (config.mode === "visual" /* Visual */) {
1255
- const visualErrors = validateVisualQueryConfig(config.visualConfig);
1256
- errors.push(...visualErrors);
1257
- } else if (config.mode === "sql" /* SQL */) {
1258
- const sqlErrors = validateSQLQueryConfig(config.sqlConfig);
1259
- errors.push(...sqlErrors);
1301
+ if (direction === "last" || direction === "next") {
1302
+ const ok = typeof amount === "number" && Number.isInteger(amount) && amount >= 1 && amount <= RELATIVE_DATE_MAX_AMOUNT;
1303
+ if (!ok) {
1304
+ issues.push(err(`${path}.relative.amount`, `${path}.relative.amount must be a whole number from 1 to ${RELATIVE_DATE_MAX_AMOUNT} for '${direction}' ` + `(e.g. last 30 days is { direction: 'last', amount: 30, unit: 'day' })`));
1305
+ }
1260
1306
  }
1261
- if (config.timeout !== undefined) {
1262
- if (config.timeout < 1000) {
1263
- errors.push({
1264
- field: "timeout",
1265
- message: "Timeout must be at least 1000ms",
1266
- severity: "error"
1267
- });
1268
- } else if (config.timeout > 300000) {
1269
- warnings.push({
1270
- field: "timeout",
1271
- message: "Timeout is very high (>5 minutes)",
1272
- severity: "warning"
1273
- });
1307
+ if (includeCurrent !== undefined && typeof includeCurrent !== "boolean") {
1308
+ issues.push(err(`${path}.relative.includeCurrent`, `${path}.relative.includeCurrent must be true or false`));
1309
+ }
1310
+ return issues;
1311
+ }
1312
+ function validateRow(path, row, inHaving) {
1313
+ const issues = [];
1314
+ if (!nonEmptyString(row.field)) {
1315
+ issues.push(err(`${path}.field`, `${path}: choose the field to compare (field is empty)`));
1316
+ }
1317
+ if (!isOneOf(QUERY_CONDITION_OPERATORS, row.operator)) {
1318
+ issues.push(err(`${path}.operator`, `${path}.operator must be one of: ${OPERATOR_LIST} (got "${String(row.operator)}")`));
1319
+ return issues;
1320
+ }
1321
+ const op = row.operator;
1322
+ const isRelative = isOneOf(QUERY_RELATIVE_DATE_OPERATORS, op);
1323
+ const comparesField = row.valueField !== undefined && row.valueField !== null && row.valueField !== "";
1324
+ if (isRelative) {
1325
+ if (inHaving) {
1326
+ issues.push(err(`${path}.operator`, `${path}: '${op}' filters dates on rows — put it in conditions, not having`));
1327
+ } else {
1328
+ issues.push(...validateRelative(path, row.relative));
1329
+ }
1330
+ if (hasValue(row.value) || comparesField) {
1331
+ issues.push(err(path, `${path}: a relative-date condition takes its window from relative — remove value and valueField`));
1274
1332
  }
1333
+ } else if (row.relative !== undefined) {
1334
+ issues.push(err(`${path}.relative`, `${path}: relative is only used with 'inRelative' or 'notInRelative' — change the operator or remove relative`));
1275
1335
  }
1276
- if (config.maxRows !== undefined) {
1277
- if (config.maxRows < 1) {
1278
- errors.push({
1279
- field: "maxRows",
1280
- message: "Max rows must be at least 1",
1281
- severity: "error"
1282
- });
1283
- } else if (config.maxRows > 1e4) {
1284
- warnings.push({
1285
- field: "maxRows",
1286
- message: "Max rows is very high (>10,000). This may impact performance.",
1287
- severity: "warning"
1288
- });
1336
+ if (comparesField) {
1337
+ if (inHaving) {
1338
+ issues.push(err(`${path}.valueField`, `${path}: having compares with a value — use value, not valueField`));
1339
+ } else if (!nonEmptyString(row.valueField)) {
1340
+ issues.push(err(`${path}.valueField`, `${path}.valueField must name the field to compare with`));
1341
+ } else if (!isOneOf(QUERY_COMPARISON_OPERATORS, op)) {
1342
+ issues.push(err(`${path}.valueField`, `${path}: comparing with another field works with =, !=, >, <, >=, <= only (got "${op}")`));
1343
+ }
1344
+ if (hasValue(row.value)) {
1345
+ issues.push(err(path, `${path}: set either value (compare with a value) or valueField (compare with another field), not both`));
1289
1346
  }
1290
1347
  }
1291
- return {
1292
- valid: errors.length === 0,
1293
- errors,
1294
- warnings
1295
- };
1296
- };
1297
- var validateVisualQueryConfig = (config) => {
1348
+ if (row.joinOperator !== undefined && !(typeof row.joinOperator === "string" && /^(and|or)$/i.test(row.joinOperator))) {
1349
+ issues.push(err(`${path}.joinOperator`, `${path}.joinOperator must be AND or OR (got "${String(row.joinOperator)}")`));
1350
+ }
1351
+ return issues;
1352
+ }
1353
+ function validateConditionList(path, list, depth) {
1354
+ if (list === undefined || list === null)
1355
+ return [];
1356
+ if (!Array.isArray(list))
1357
+ return [err(path, `${path} must be a list of conditions`)];
1358
+ const issues = [];
1359
+ list.forEach((item, i) => {
1360
+ const at = `${path}[${i}]`;
1361
+ if (!isRecord(item)) {
1362
+ issues.push(err(at, `${at} must be a condition ({ field, operator, value }) or a group ({ type: 'group', conditions })`));
1363
+ return;
1364
+ }
1365
+ if (isConditionGroup(item)) {
1366
+ if (depth + 1 > MAX_CONDITION_DEPTH) {
1367
+ issues.push(err(at, `${at}: groups can nest at most ${MAX_CONDITION_DEPTH} levels deep — move this group up a level or combine its conditions`));
1368
+ return;
1369
+ }
1370
+ if (item.joinOperator !== undefined && !(typeof item.joinOperator === "string" && /^(and|or)$/i.test(item.joinOperator))) {
1371
+ issues.push(err(`${at}.joinOperator`, `${at}.joinOperator must be AND or OR (got "${String(item.joinOperator)}")`));
1372
+ }
1373
+ if (!Array.isArray(item.conditions) || item.conditions.length === 0) {
1374
+ issues.push(err(`${at}.conditions`, `${at}: a group needs at least one condition — add one or remove the group`));
1375
+ return;
1376
+ }
1377
+ issues.push(...validateConditionList(`${at}.conditions`, item.conditions, depth + 1));
1378
+ return;
1379
+ }
1380
+ issues.push(...validateRow(at, item, false));
1381
+ });
1382
+ return issues;
1383
+ }
1384
+ function validateAggregates(vc) {
1385
+ const list = vc.aggregates;
1386
+ if (list === undefined || list === null)
1387
+ return [];
1388
+ if (!Array.isArray(list))
1389
+ return [err("visualConfig.aggregates", "visualConfig.aggregates must be a list of { fn, field, alias }")];
1390
+ const issues = [];
1391
+ list.forEach((a, i) => {
1392
+ const at = `visualConfig.aggregates[${i}]`;
1393
+ if (!isRecord(a)) {
1394
+ issues.push(err(at, `${at} must be { fn, field, alias }`));
1395
+ return;
1396
+ }
1397
+ const fn = typeof a.fn === "string" ? a.fn.toUpperCase() : a.fn;
1398
+ if (!isOneOf(QUERY_AGGREGATE_FUNCTIONS, fn)) {
1399
+ issues.push(err(`${at}.fn`, `${at}.fn must be one of: ${QUERY_AGGREGATE_FUNCTIONS.join(", ")} (got "${String(a.fn)}")`));
1400
+ return;
1401
+ }
1402
+ const allRows = !nonEmptyString(a.field) || a.field.trim() === "*";
1403
+ if (allRows && fn !== "COUNT") {
1404
+ issues.push(err(`${at}.field`, `${at}: ${fn} needs a field to work on — set field to a column name`));
1405
+ }
1406
+ if (a.alias !== undefined && a.alias !== null && !nonEmptyString(a.alias)) {
1407
+ issues.push(err(`${at}.alias`, `${at}.alias must be a name for the result column, or left out`));
1408
+ }
1409
+ });
1410
+ return issues;
1411
+ }
1412
+ function validateHaving(vc) {
1413
+ const list = vc.having;
1414
+ if (list === undefined || list === null || Array.isArray(list) && list.length === 0)
1415
+ return [];
1416
+ if (!Array.isArray(list))
1417
+ return [err("visualConfig.having", "visualConfig.having must be a list of conditions")];
1418
+ const aggregates = arr(vc.aggregates).filter(isRecord);
1419
+ const groupBy = arr(vc.groupBy).filter(nonEmptyString);
1420
+ if (aggregates.length === 0 && groupBy.length === 0) {
1421
+ return [err("visualConfig.having", "visualConfig.having filters grouped results, so it needs aggregates or groupBy — add them, or move these filters to conditions")];
1422
+ }
1423
+ const names = new Set([
1424
+ ...groupBy,
1425
+ ...aggregates.map((a) => a.alias).filter(nonEmptyString)
1426
+ ]);
1427
+ const issues = [];
1428
+ list.forEach((row, i) => {
1429
+ const at = `visualConfig.having[${i}]`;
1430
+ if (!isRecord(row) || isConditionGroup(row)) {
1431
+ issues.push(err(at, `${at} must be a single condition ({ field, operator, value }); groups are not used in having`));
1432
+ return;
1433
+ }
1434
+ issues.push(...validateRow(at, row, true));
1435
+ if (nonEmptyString(row.field) && !names.has(row.field)) {
1436
+ const known = [...names];
1437
+ issues.push(err(`${at}.field`, `${at}.field must be an aggregate alias or a groupBy column (got "${row.field}"` + (known.length > 0 ? `; available: ${known.join(", ")})` : "; give the aggregate an alias first)")));
1438
+ }
1439
+ });
1440
+ return issues;
1441
+ }
1442
+ function validateVisualConfig(config) {
1443
+ const vc = config.visualConfig;
1444
+ if (!isRecord(vc)) {
1445
+ return [err("visualConfig", "visualConfig is required — describe the query with tables (what to read), fields (which columns) and optional conditions")];
1446
+ }
1447
+ const issues = [];
1448
+ if (!queryTablesSatisfied(config)) {
1449
+ issues.push(err("visualConfig.tables", "At least one table must be selected (visualConfig.tables)"));
1450
+ }
1451
+ if (!queryFieldsSatisfied(config)) {
1452
+ issues.push(err("visualConfig.fields", "At least one field must be selected (visualConfig.fields), or add aggregates / groupBy for a summary query"));
1453
+ }
1454
+ if (arr(vc.tables).length > 1 && arr(vc.joins).length === 0) {
1455
+ issues.push(err("visualConfig.joins", "Joins are required when selecting from multiple tables (visualConfig.joins)"));
1456
+ }
1457
+ if (vc.distinct !== undefined && typeof vc.distinct !== "boolean") {
1458
+ issues.push(err("visualConfig.distinct", "visualConfig.distinct must be true or false"));
1459
+ }
1460
+ issues.push(...validateConditionList("visualConfig.conditions", vc.conditions, 0));
1461
+ issues.push(...validateAggregates(vc));
1462
+ issues.push(...validateHaving(vc));
1463
+ return issues;
1464
+ }
1465
+ var validateBuildQueryConfig = (config) => {
1466
+ const c = config;
1298
1467
  const errors = [];
1299
- if (!config) {
1300
- errors.push({
1301
- field: "visualConfig",
1302
- message: "Visual query configuration is required",
1303
- severity: "error"
1304
- });
1305
- return errors;
1468
+ const warnings = [];
1469
+ if (!c.connectorId) {
1470
+ errors.push(err("connectorId", "Please select a connector (connectorId)"));
1306
1471
  }
1307
- if (!config.tables || config.tables.length === 0) {
1308
- errors.push({
1309
- field: "visualConfig.tables",
1310
- message: "At least one table must be selected",
1311
- severity: "error"
1312
- });
1472
+ const mode = c.mode;
1473
+ const visual = mode === undefined || mode === null || mode === "" || isOneOf(QUERY_MODES, mode);
1474
+ if (!visual) {
1475
+ errors.push(err("mode", `Mode must be one of: ${QUERY_MODES.join(", ")} (got "${String(mode)}"). ${QUERY_VISUAL_ONLY_MESSAGE}`));
1476
+ } else if (!mode) {
1477
+ errors.push(err("mode", "Query mode is required — set mode to 'visual'"));
1313
1478
  }
1314
- if (!config.fields || config.fields.length === 0) {
1315
- errors.push({
1316
- field: "visualConfig.fields",
1317
- message: "At least one field must be selected",
1318
- severity: "error"
1319
- });
1479
+ const queryType = c.queryType;
1480
+ if (!queryType) {
1481
+ errors.push(err("queryType", "Query type is required — set queryType to 'select'"));
1482
+ } else if (!isOneOf(QUERY_TYPES, queryType)) {
1483
+ errors.push(err("queryType", `Query type must be one of: ${QUERY_TYPES.join(", ")} (got "${String(queryType)}"). ` + "The Query step only reads data — use a Transform step to insert, update or delete."));
1320
1484
  }
1321
- if (config.tables && config.tables.length > 1) {
1322
- if (!config.joins || config.joins.length === 0) {
1323
- errors.push({
1324
- field: "visualConfig.joins",
1325
- message: "Joins are required when selecting from multiple tables",
1326
- severity: "error"
1327
- });
1485
+ if (visual) {
1486
+ const sql = isRecord(c.sqlConfig) ? c.sqlConfig.sql : undefined;
1487
+ if (nonEmptyString(sql) && !isRecord(c.visualConfig)) {
1488
+ errors.push(err("sqlConfig.sql", QUERY_AUTHORED_SQL_MESSAGE));
1489
+ } else {
1490
+ errors.push(...validateVisualConfig(c));
1328
1491
  }
1329
1492
  }
1330
- const hasAggregates = config.fields?.some((field) => field.aggregate);
1331
- const hasNonAggregates = config.fields?.some((field) => !field.aggregate);
1332
- if (hasAggregates && hasNonAggregates) {
1333
- if (!config.groupBy || config.groupBy.length === 0) {
1334
- errors.push({
1335
- field: "visualConfig.groupBy",
1336
- message: "Group by is required when mixing aggregate and non-aggregate fields",
1337
- severity: "error"
1338
- });
1493
+ const timeout = c.timeout;
1494
+ if (typeof timeout === "number") {
1495
+ if (timeout < 1000) {
1496
+ errors.push(err("timeout", "Timeout must be at least 1000ms"));
1497
+ } else if (timeout > 300000) {
1498
+ warnings.push({ field: "timeout", message: "Timeout is very high (>5 minutes)", severity: "warning" });
1339
1499
  }
1340
1500
  }
1341
- return errors;
1342
- };
1343
- var validateSQLQueryConfig = (config) => {
1344
- const errors = [];
1345
- if (!config) {
1346
- errors.push({
1347
- field: "sqlConfig",
1348
- message: "SQL query configuration is required",
1349
- severity: "error"
1350
- });
1351
- return errors;
1352
- }
1353
- if (!config.sql || config.sql.trim().length === 0) {
1354
- errors.push({
1355
- field: "sqlConfig.sql",
1356
- message: "SQL query is required",
1357
- severity: "error"
1358
- });
1359
- } else {
1360
- const sql = config.sql.trim().toLowerCase();
1361
- const dangerousKeywords = ["drop", "truncate", "alter", "create", "grant", "revoke"];
1362
- const hasDangerousKeyword = dangerousKeywords.some((keyword) => sql.includes(keyword + " "));
1363
- if (hasDangerousKeyword) {
1364
- errors.push({
1365
- field: "sqlConfig.sql",
1366
- message: "SQL query contains potentially dangerous operations (DROP, TRUNCATE, ALTER, etc.)",
1367
- severity: "error"
1368
- });
1501
+ const maxRows = c.maxRows;
1502
+ if (typeof maxRows === "number") {
1503
+ if (maxRows < 1) {
1504
+ errors.push(err("maxRows", "Max rows must be at least 1"));
1505
+ } else if (maxRows > 1e4) {
1506
+ warnings.push({ field: "maxRows", message: "Max rows is very high (>10,000). This may impact performance.", severity: "warning" });
1369
1507
  }
1370
1508
  }
1371
- return errors;
1509
+ return { valid: errors.length === 0, errors, warnings };
1372
1510
  };
1373
1511
  function isQueryableConnector(connector) {
1374
1512
  if (connector.isQueryable !== undefined) {
@@ -1400,17 +1538,26 @@ var BuildQueryContractSchema = {
1400
1538
  mode: {
1401
1539
  type: "select",
1402
1540
  required: true,
1403
- options: ["visual", "sql"],
1404
- description: "visual: the query is built from visualConfig; sql: the query is the text in sqlConfig.sql"
1541
+ options: QUERY_MODES,
1542
+ description: "Always 'visual': the Query step is built with the visual builder and described in visualConfig. Hand-written SQL is not accepted."
1405
1543
  },
1406
1544
  queryType: {
1407
1545
  type: "select",
1408
1546
  required: true,
1409
- options: ["select", "insert", "update", "delete", "custom"],
1410
- description: "Only select is a read; the write types exist for the SQL mode of database connectors"
1547
+ options: QUERY_TYPES,
1548
+ description: "Always 'select': the Query step reads data. Use a Transform step to write."
1549
+ },
1550
+ visualConfig: {
1551
+ type: "object",
1552
+ required: false,
1553
+ description: 'The query. tables: the table or object to read; fields: "table.column" names to return; ' + "conditions: filters { field, operator, value } (or valueField to compare two fields, or operator " + "'inRelative' with relative { direction: last|next|this, amount, unit: day|week|month|quarter|year } for date windows), " + `which may be grouped { type: 'group', conditions } up to ${MAX_CONDITION_DEPTH} levels deep and joined with AND/OR; ` + "distinct; groupBy; aggregates [{ fn: COUNT|COUNT_DISTINCT|SUM|AVG|MIN|MAX, field, alias }]; " + "having: filters on aggregate aliases or groupBy columns; sort [{ field, direction }]; maxRows: the run cap (up to 10,000)."
1554
+ },
1555
+ sqlConfig: {
1556
+ type: "object",
1557
+ required: false,
1558
+ derived: true,
1559
+ description: "Compiled from visualConfig by the editor or the server. Never write it — describe the query in visualConfig."
1411
1560
  },
1412
- visualConfig: { type: "object", required: false },
1413
- sqlConfig: { type: "object", required: false },
1414
1561
  timeout: { type: "number", required: false },
1415
1562
  maxRows: { type: "number", required: false }
1416
1563
  };
@@ -1419,11 +1566,11 @@ var BuildQueryContractSchema = {
1419
1566
  var queryDefinition = {
1420
1567
  id: "query",
1421
1568
  name: "Query",
1422
- description: "Execute queries against connected databases, APIs, and CRMs",
1423
- longDescription: "Create powerful queries using either a visual no-code builder or advanced SQL editor. Supports databases (PostgreSQL, MySQL, MongoDB, etc.), APIs (REST, GraphQL), and CRMs (Salesforce, HubSpot). Query results can be used as input for downstream actions.",
1569
+ description: "Read records from a connected database, app or CRM",
1570
+ longDescription: "Pick a table or object, the fields you want and any filters, and the Query step reads the matching records. Filters can be grouped with AND / OR, compare two fields, or use a date window such as the last 30 days. You can also remove duplicates, count, total or average values by group, filter those totals, and sort the result. Works with databases (PostgreSQL, MySQL, SQL Server, MongoDB and more), apps (QuickBooks, HubSpot, Jira, Shopify, Google Sheets and more) and CRMs (Salesforce). The records it returns can be used by the steps that follow.",
1424
1571
  category: "database" /* Database */,
1425
1572
  type: "query" /* Query */,
1426
- tags: ["query", "database", "sql", "visual-builder", "no-code", "api", "crm", "built-in"],
1573
+ tags: ["query", "database", "visual-builder", "no-code", "filter", "api", "crm", "built-in"],
1427
1574
  status: "stable" /* Stable */,
1428
1575
  icon: "QueryQueue",
1429
1576
  color: "#0043ce",
@@ -1441,17 +1588,12 @@ var queryDefinition = {
1441
1588
  key: "mode",
1442
1589
  label: "Query Mode",
1443
1590
  type: "select" /* Select */,
1444
- description: "Choose between visual query builder or SQL editor",
1591
+ description: "Always 'visual' — the query is built with the visual builder",
1445
1592
  options: [
1446
1593
  {
1447
- label: "Visual Builder (No-Code)",
1594
+ label: "Visual Builder",
1448
1595
  value: "visual" /* Visual */,
1449
- description: "Build queries visually without writing code"
1450
- },
1451
- {
1452
- label: "SQL Editor (Advanced)",
1453
- value: "sql" /* SQL */,
1454
- description: "Write raw SQL queries with syntax highlighting"
1596
+ description: "Pick the table, fields and filters — no code needed"
1455
1597
  }
1456
1598
  ],
1457
1599
  defaultValue: "visual" /* Visual */,
@@ -1499,7 +1641,7 @@ var queryDefinition = {
1499
1641
  key: "visualConfig.fields",
1500
1642
  label: "Select Fields",
1501
1643
  type: "array" /* Array */,
1502
- description: "Choose fields to include in the query result",
1644
+ description: 'The fields to return, as "table.column". Not needed for a summary query (aggregates / groupBy) or when every table and field is selected.',
1503
1645
  validation: { required: true },
1504
1646
  conditional: [
1505
1647
  {
@@ -1542,39 +1684,6 @@ var queryDefinition = {
1542
1684
  }
1543
1685
  ]
1544
1686
  },
1545
- {
1546
- key: "sqlConfig.sql",
1547
- label: "SQL Query",
1548
- type: "code" /* Code */,
1549
- language: "sql",
1550
- description: "Write your SQL query here",
1551
- placeholder: "SELECT * FROM users WHERE status = 'active' LIMIT 100",
1552
- validation: { required: true },
1553
- conditional: [
1554
- {
1555
- field: "mode",
1556
- operator: "==",
1557
- value: "sql" /* SQL */,
1558
- action: "show"
1559
- }
1560
- ]
1561
- },
1562
- {
1563
- key: "sqlConfig.parameters",
1564
- label: "Query Parameters",
1565
- type: "json" /* JSON */,
1566
- description: 'Parameters for parameterized queries (e.g., {"userId": 123})',
1567
- validation: { required: false },
1568
- advanced: true,
1569
- conditional: [
1570
- {
1571
- field: "mode",
1572
- operator: "==",
1573
- value: "sql" /* SQL */,
1574
- action: "show"
1575
- }
1576
- ]
1577
- },
1578
1687
  {
1579
1688
  key: "timeout",
1580
1689
  label: "Query Timeout (ms)",
@@ -1588,7 +1697,7 @@ var queryDefinition = {
1588
1697
  key: "maxRows",
1589
1698
  label: "Max Rows",
1590
1699
  type: "number" /* Number */,
1591
- description: "Legacy step-level field — NOT read at run. Cap a visual Query with visualConfig.maxRows; in SQL mode write a LIMIT.",
1700
+ description: "Legacy step-level field — not read at run. To cap a run, set visualConfig.maxRows.",
1592
1701
  defaultValue: 1000,
1593
1702
  validation: { required: false, min: 1, max: 1e4 },
1594
1703
  advanced: true
@@ -1604,7 +1713,7 @@ var queryDefinition = {
1604
1713
  {
1605
1714
  id: "query_mode",
1606
1715
  label: "Query Configuration",
1607
- description: "Configure how you want to build your query",
1716
+ description: "The query is built with the visual builder",
1608
1717
  fields: ["mode", "queryType"]
1609
1718
  },
1610
1719
  {
@@ -1613,12 +1722,6 @@ var queryDefinition = {
1613
1722
  description: "Build your query visually without writing code",
1614
1723
  fields: ["visualConfig.tables", "visualConfig.fields", "visualConfig.limit"]
1615
1724
  },
1616
- {
1617
- id: "sql_editor",
1618
- label: "SQL Editor",
1619
- description: "Write raw SQL queries",
1620
- fields: ["sqlConfig.sql", "sqlConfig.parameters"]
1621
- },
1622
1725
  {
1623
1726
  id: "settings",
1624
1727
  label: "Advanced Settings",
@@ -1658,7 +1761,7 @@ var queryDefinition = {
1658
1761
  {
1659
1762
  key: "generatedSQL",
1660
1763
  type: "string" /* String */,
1661
- description: "The SQL query that was executed (for visual builder mode)",
1764
+ description: "The query that ran, compiled from the visual builder",
1662
1765
  example: "SELECT id, name, email FROM users WHERE status = 'active' LIMIT 100"
1663
1766
  }
1664
1767
  ],
@@ -1784,6 +1887,32 @@ var aggregateDefinition = {
1784
1887
  testable: false
1785
1888
  };
1786
1889
 
1890
+ // src/catalog/storage/fileHelp.ts
1891
+ var FILE_OUTPUT_HELP = {
1892
+ operation: "The operation that ran",
1893
+ data: "File contents, for text files such as CSV, JSON or TXT",
1894
+ s3Key: "The file itself, for PDF, Excel, images and other non-text files. Pass it to Excel, CSV, IDP or an Upload step.",
1895
+ fileName: "File name",
1896
+ mimeType: "File type, e.g. text/csv or application/pdf",
1897
+ path: "Where the file was read from"
1898
+ };
1899
+ var FILE_INPUT_HELP = {
1900
+ uploadContent: "Leave blank to upload the file from the step before, or pick a value from an earlier step, or type the content.",
1901
+ formatContent: (format) => `The ${format} from an earlier step (a download or a generated file), or paste the ${format} here.`,
1902
+ excelContent: "The Excel file from an earlier step (a download or a generated file)."
1903
+ };
1904
+ var STORAGE_DOWNLOAD_OUTPUT_SCHEMA = {
1905
+ type: "object",
1906
+ properties: {
1907
+ operation: { type: "string", description: FILE_OUTPUT_HELP.operation },
1908
+ data: { type: "string", description: FILE_OUTPUT_HELP.data },
1909
+ s3Key: { type: "string", description: FILE_OUTPUT_HELP.s3Key },
1910
+ fileName: { type: "string", description: FILE_OUTPUT_HELP.fileName },
1911
+ mimeType: { type: "string", description: FILE_OUTPUT_HELP.mimeType },
1912
+ path: { type: "string", description: FILE_OUTPUT_HELP.path }
1913
+ }
1914
+ };
1915
+
1787
1916
  // src/catalog/storage/csv-action/index.ts
1788
1917
  var csvActionDefinition = {
1789
1918
  id: "csv_action",
@@ -1798,7 +1927,7 @@ var csvActionDefinition = {
1798
1927
  inputSchema: {
1799
1928
  fields: [
1800
1929
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (CSV → Records)", value: "parse" }, { label: "Generate (Records → CSV)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1801
- { key: "content", label: "CSV Content", type: "string" /* String */, description: "Paste CSV content or use FX token from upstream step" },
1930
+ { key: "content", label: "CSV Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("CSV") },
1802
1931
  { key: "delimiter", label: "Delimiter", type: "select" /* Select */, options: [{ label: "Comma (,)", value: "," }, { label: "Semicolon (;)", value: ";" }, { label: "Tab", value: "\t" }, { label: "Pipe (|)", value: "|" }], defaultValue: "," },
1803
1932
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing column headers (default: 1)", defaultValue: 1 },
1804
1933
  { key: "encoding", label: "Encoding", type: "select" /* Select */, options: [{ label: "UTF-8", value: "utf-8" }, { label: "Latin-1", value: "latin1" }, { label: "ASCII", value: "ascii" }], defaultValue: "utf-8" },
@@ -1838,7 +1967,7 @@ var jsonActionDefinition = {
1838
1967
  fields: [
1839
1968
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (JSON → Records)", value: "parse" }, { label: "Generate (Records → JSON)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1840
1969
  { key: "format", label: "Format", type: "select" /* Select */, options: [{ label: "JSON (array)", value: "json" }, { label: "JSONL (one object per line)", value: "jsonl" }], defaultValue: "json" },
1841
- { key: "content", label: "JSON Content", type: "string" /* String */, description: "Paste JSON content or use FX token from upstream step" },
1970
+ { key: "content", label: "JSON Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("JSON") },
1842
1971
  { key: "prettyPrint", label: "Pretty Print", type: "boolean" /* Boolean */, description: "Format output with indentation", defaultValue: true },
1843
1972
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.json" }
1844
1973
  ]
@@ -1874,7 +2003,7 @@ var xmlActionDefinition = {
1874
2003
  inputSchema: {
1875
2004
  fields: [
1876
2005
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (XML → Records)", value: "parse" }, { label: "Generate (Records → XML)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1877
- { key: "content", label: "XML Content", type: "string" /* String */, description: "Paste XML or use FX token" },
2006
+ { key: "content", label: "XML Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("XML") },
1878
2007
  { key: "rootElement", label: "Root Element", type: "string" /* String */, description: "Root XML element name (for generate)", defaultValue: "data" },
1879
2008
  { key: "rowElement", label: "Row Element", type: "string" /* String */, description: "Element name per record (for generate)", defaultValue: "row" },
1880
2009
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xml" }
@@ -1911,7 +2040,7 @@ var excelActionDefinition = {
1911
2040
  inputSchema: {
1912
2041
  fields: [
1913
2042
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (Excel → Records)", value: "parse" }, { label: "Generate (Records → Excel)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1914
- { key: "content", label: "Excel File", type: "string" /* String */, description: "S3 key of uploaded Excel file or FX token from upstream" },
2043
+ { key: "content", label: "Excel File", type: "string" /* String */, description: FILE_INPUT_HELP.excelContent },
1915
2044
  { key: "sheetName", label: "Sheet Name", type: "string" /* String */, description: "Name of sheet to read (default: first sheet)" },
1916
2045
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing headers", defaultValue: 1 },
1917
2046
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xlsx" }
@@ -1923,7 +2052,7 @@ var excelActionDefinition = {
1923
2052
  { key: "data", type: "array" /* Array */, description: "Parsed records (parse mode) — loop these with a ForEach. File metadata in generate mode." },
1924
2053
  { key: "columns", type: "json" /* JSON */, description: "Column names" },
1925
2054
  { key: "rowCount", type: "number" /* Number */, description: "Number of records" },
1926
- { key: "s3Key", type: "string" /* String */, description: "S3 key of generated file" }
2055
+ { key: "s3Key", type: "string" /* String */, description: "The generated Excel file. Pass it to an Upload step to save it." }
1927
2056
  ],
1928
2057
  sampleOutput: { data: [{ id: 1, name: "Widget" }], columns: ["id", "name"], rowCount: 1 }
1929
2058
  },
@@ -1953,7 +2082,7 @@ var s3ActionDefinition = {
1953
2082
  { key: "bucket", label: "Bucket Name", type: "string" /* String */, description: "S3 bucket name (defaults to the connector’s bucket)" },
1954
2083
  { key: "key", label: "Object Key (Path)", type: "string" /* String */, description: "S3 object key, e.g. data/reports/monthly.csv" },
1955
2084
  { key: "prefix", label: "Prefix (for List)", type: "string" /* String */, description: "Filter objects by prefix (folder path)" },
1956
- { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
2085
+ { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1957
2086
  { key: "contentType", label: "Content Type", type: "string" /* String */, description: "MIME type (e.g. text/csv, application/json)", defaultValue: "application/octet-stream" },
1958
2087
  { key: "destBucket", label: "Destination Bucket (for Copy)", type: "string" /* String */ },
1959
2088
  { key: "destKey", label: "Destination Key (for Copy)", type: "string" /* String */ },
@@ -1994,7 +2123,7 @@ var ftpActionDefinition = {
1994
2123
  { key: "connector", label: "FTP / SFTP Connection", type: "connection" /* Connection */, description: "Select a configured FTP/SFTP connector from Integrations", validation: { required: true } },
1995
2124
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete File", value: "delete" }, { label: "Move / Rename", value: "move" }], defaultValue: "download", validation: { required: true } },
1996
2125
  { key: "remotePath", label: "Remote Path", type: "string" /* String */, description: "File or folder path on the server (use file explorer to browse)" },
1997
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
2126
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1998
2127
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */, description: "Name for the uploaded file" },
1999
2128
  { key: "destPath", label: "Destination Path (Move)", type: "string" /* String */, description: "New path for moved/renamed file" },
2000
2129
  { key: "ifExists", label: "If File Exists", type: "select" /* Select */, options: [{ label: "Overwrite", value: "overwrite" }, { label: "Skip", value: "skip" }], defaultValue: "overwrite" }
@@ -2035,7 +2164,7 @@ var googleDriveActionDefinition = {
2035
2164
  { key: "fileId", label: "File ID / Path", type: "string" /* String */, description: "Google Drive file ID (use file explorer to browse)" },
2036
2165
  { key: "folderId", label: "Folder ID", type: "string" /* String */, description: "Target folder (root if empty)" },
2037
2166
  { key: "fileName", label: "File Name", type: "string" /* String */ },
2038
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2167
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2039
2168
  { key: "mimeType", label: "MIME Type", type: "string" /* String */, defaultValue: "application/octet-stream" },
2040
2169
  { key: "destFolderId", label: "Destination Folder (Move/Copy)", type: "string" /* String */ },
2041
2170
  { key: "shareEmail", label: "Share With Email", type: "string" /* String */ },
@@ -2077,7 +2206,7 @@ var dropboxActionDefinition = {
2077
2206
  { key: "connector", label: "Dropbox Account", type: "connection" /* Connection */, description: "Select a connected Dropbox account", validation: { required: true } },
2078
2207
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }, { label: "Create Folder", value: "create_folder" }, { label: "Get Shared Link", value: "shared_link" }], defaultValue: "download", validation: { required: true } },
2079
2208
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on Dropbox (use file explorer to browse)" },
2080
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2209
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2081
2210
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
2082
2211
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ },
2083
2212
  { key: "mode", label: "Upload Mode", type: "select" /* Select */, options: [{ label: "Add (fail if exists)", value: "add" }, { label: "Overwrite", value: "overwrite" }], defaultValue: "overwrite" }
@@ -2117,7 +2246,7 @@ var onedriveActionDefinition = {
2117
2246
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }], defaultValue: "download", validation: { required: true } },
2118
2247
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on OneDrive (use file explorer to browse)" },
2119
2248
  { key: "itemId", label: "Item ID", type: "string" /* String */, description: "OneDrive item ID (alternative to path)" },
2120
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2249
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2121
2250
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
2122
2251
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ }
2123
2252
  ]
@@ -2175,14 +2304,17 @@ var storageActionDefinition = {
2175
2304
  },
2176
2305
  { key: "path", label: "Path", type: "string" /* String */, description: "File or folder path — use the file browser in the panel." },
2177
2306
  { key: "destPath", label: "Destination path", type: "string" /* String */, description: "Target for copy / move." },
2178
- { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: "Content to upload — bind an FX token from an upstream step." }
2307
+ { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent }
2179
2308
  ]
2180
2309
  },
2181
2310
  outputSchema: {
2182
2311
  type: "object",
2183
2312
  properties: [
2184
2313
  { key: "operation", type: "string" /* String */, description: "The operation that ran" },
2185
- { key: "data", type: "json" /* JSON */, description: "Operation result or downloaded content" },
2314
+ { key: "data", type: "json" /* JSON */, description: `Download: ${FILE_OUTPUT_HELP.data}. Other operations: their result.` },
2315
+ { key: "s3Key", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.s3Key}` },
2316
+ { key: "fileName", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.fileName}` },
2317
+ { key: "mimeType", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.mimeType}` },
2186
2318
  { key: "files", type: "json" /* JSON */, description: "Listing (for the List operation)" },
2187
2319
  { key: "path", type: "string" /* String */, description: "Affected file/folder path" },
2188
2320
  { key: "url", type: "string" /* String */, description: "Signed URL (for the Get signed URL operation)" }
@@ -2824,6 +2956,10 @@ function isSatisfiedOutOfBand(actionId, fieldKey, config) {
2824
2956
  return true;
2825
2957
  return Array.isArray(config.blocks) && config.blocks.length > 0;
2826
2958
  }
2959
+ if (actionId === "query" && fieldKey === "visualConfig.fields")
2960
+ return queryFieldsSatisfied(config);
2961
+ if (actionId === "query" && fieldKey === "visualConfig.tables")
2962
+ return queryTablesSatisfied(config);
2827
2963
  return false;
2828
2964
  }
2829
2965
  var MESSAGE_BODY_ACTIONS = new Set([
@@ -3718,9 +3854,6 @@ var EmailConfig = {
3718
3854
  }
3719
3855
  }
3720
3856
  };
3721
- // src/catalog/isOneOf.ts
3722
- var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3723
-
3724
3857
  // src/catalog/general/transform/config.ts
3725
3858
  var TRANSFORM_MODES = ["visual", "expression"];
3726
3859
  var TRANSFORM_OPERATIONS = [
@@ -4628,10 +4761,10 @@ var FILE_EDIT_SCHEMA = {
4628
4761
  matchOp: { type: "select", required: false, options: FILE_EDIT_MATCH_OPS, description: 'set_fields: comparison; "=" by default' },
4629
4762
  matchValue: { type: "string", required: false, description: "set_fields: value compared against" },
4630
4763
  setFields: { type: "object", required: false, description: "set_fields: { column: newValue } applied to matching rows" },
4631
- destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites an S3 source' },
4764
+ destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites the source file' },
4632
4765
  outputFileName: { type: "string", required: false, description: 'Name of the edited copy when destination is "new"' },
4633
4766
  source: { type: "select", required: false, options: FILE_EDIT_SOURCES, description: "Where the file to edit comes from; inferred from content when absent" },
4634
- s3Key: { type: "string", required: false, description: 'source "s3": platform-bucket key of the file to edit' },
4767
+ s3Key: { type: "string", required: false, description: `source "s3": an earlier step's file (its s3Key output) to edit` },
4635
4768
  url: { type: "url", required: false, description: 'source "url": the file to edit is fetched from here' }
4636
4769
  };
4637
4770
  function fileEditErrors(config) {
@@ -4663,7 +4796,7 @@ var CsvActionConfig = {
4663
4796
  },
4664
4797
  schema: {
4665
4798
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (CSV → records), generate (records → CSV) or edit an existing file" },
4666
- content: { type: "text", required: false, description: "CSV text, or an FX token from an upstream step" },
4799
+ content: { type: "text", required: false, description: "CSV text, or a reference to an earlier step's file content" },
4667
4800
  delimiter: { type: "select", required: false, options: [",", ";", "\t", "|"] },
4668
4801
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4669
4802
  encoding: { type: "string", required: false, description: "Text encoding (utf-8 by default)" },
@@ -4689,7 +4822,7 @@ var JsonActionConfig = {
4689
4822
  schema: {
4690
4823
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (JSON → records), generate (records → JSON) or edit an existing file" },
4691
4824
  format: { type: "select", required: false, options: ["json", "jsonl"], description: "json (array) or jsonl (one object per line)" },
4692
- content: { type: "text", required: false, description: "JSON text, or an FX token from an upstream step" },
4825
+ content: { type: "text", required: false, description: "JSON text, or a reference to an earlier step's file content" },
4693
4826
  prettyPrint: { type: "boolean", required: false, description: "generate: indent the output" },
4694
4827
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
4695
4828
  jsonl: { type: "boolean", required: false, description: "edit: treat the file as JSONL" },
@@ -4716,7 +4849,7 @@ var XmlActionConfig = {
4716
4849
  },
4717
4850
  schema: {
4718
4851
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (XML → records), generate (records → XML) or edit an existing file" },
4719
- content: { type: "text", required: false, description: "XML text, or an FX token from an upstream step" },
4852
+ content: { type: "text", required: false, description: "XML text, or a reference to an earlier step's file content" },
4720
4853
  rootElement: { type: "string", required: false, description: "generate: root element name" },
4721
4854
  rowElement: { type: "string", required: false, description: "Element that holds one record" },
4722
4855
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4738,7 +4871,7 @@ var ExcelActionConfig = {
4738
4871
  },
4739
4872
  schema: {
4740
4873
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (Excel → records), generate (records → Excel) or edit an existing file" },
4741
- content: { type: "text", required: false, description: "Platform-bucket key of the workbook, base64 bytes, or an FX token from an upstream step" },
4874
+ content: { type: "text", required: false, description: "A reference to an earlier step's Excel file (its s3Key output), or base64 bytes" },
4742
4875
  sheetName: { type: "string", required: false, description: "Sheet to read; the first sheet when absent" },
4743
4876
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4744
4877
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4756,7 +4889,7 @@ var FIXED_SCHEMA = {
4756
4889
  connector: { type: "connection", required: true, description: "Storage connector instance; its provider decides the operations" },
4757
4890
  operation: { type: "select", required: true, description: "Per provider: list, download, upload, delete, copy, move, presign, share, create_folder, transfer, sync" },
4758
4891
  outputAs: { type: "string", required: false, description: "Name of this step's output in the run's data bag" },
4759
- content: { type: "text", required: false, description: "upload: the body" },
4892
+ content: { type: "text", required: false, description: "upload: the file content; blank uploads the file the step before produced" },
4760
4893
  destConnector: { type: "connection", required: false, description: "transfer / sync: destination connector instance" },
4761
4894
  destPath: { type: "string", required: false, description: "transfer / sync: destination path; also move / copy destination on path-based providers" },
4762
4895
  sourceIsDirectory: { type: "boolean", required: false, description: "transfer: copy a folder recursively" },