@octaviaflow/flow-rules 0.18.0 → 0.19.1

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,
@@ -78,19 +77,29 @@ __export(exports_src, {
78
77
  GroupConfig: () => GroupConfig,
79
78
  IDP_EXTRACT_STRATEGIES: () => IDP_EXTRACT_STRATEGIES,
80
79
  IdpExtractConfig: () => IdpExtractConfig,
81
- JoinType: () => JoinType,
82
80
  JsonActionConfig: () => JsonActionConfig,
83
- LogicalOperator: () => LogicalOperator,
84
81
  MAPPING_FUNCTIONS: () => MAPPING_FUNCTIONS,
85
82
  MAPPING_FUNCTION_SPECS: () => MAPPING_FUNCTION_SPECS,
83
+ MAX_CONDITION_DEPTH: () => MAX_CONDITION_DEPTH,
86
84
  MAX_STEP_CONFIG_BYTES: () => MAX_STEP_CONFIG_BYTES,
87
85
  ManualTriggerConfig: () => ManualTriggerConfig,
88
86
  NodeStatus: () => NodeStatus,
89
87
  ONEDRIVE_OPERATIONS: () => ONEDRIVE_OPERATIONS,
90
88
  OneDriveActionConfig: () => OneDriveActionConfig,
91
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,
92
98
  QueryMode: () => QueryMode,
93
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,
94
103
  S3ActionConfig: () => S3ActionConfig,
95
104
  S3_OPERATIONS: () => S3_OPERATIONS,
96
105
  STORAGE_CONNECTOR_IDS: () => STORAGE_CONNECTOR_IDS,
@@ -134,6 +143,7 @@ __export(exports_src, {
134
143
  getStorageProvider: () => getStorageProvider,
135
144
  groupKeyName: () => groupKeyName,
136
145
  inferSchemaFromSample: () => inferSchemaFromSample,
146
+ isConditionGroup: () => isConditionGroup,
137
147
  isEditorPrivateKey: () => isEditorPrivateKey,
138
148
  isEventCapableConnector: () => isEventCapableConnector,
139
149
  isMappingFunction: () => isMappingFunction,
@@ -141,6 +151,8 @@ __export(exports_src, {
141
151
  mappingTransformFunctionName: () => mappingTransformFunctionName,
142
152
  migrateDelayConfig: () => migrateDelayConfig,
143
153
  queryDefinition: () => queryDefinition,
154
+ queryFieldsSatisfied: () => queryFieldsSatisfied,
155
+ queryTablesSatisfied: () => queryTablesSatisfied,
144
156
  resolveRecordSchema: () => resolveRecordSchema,
145
157
  selectsWithoutValues: () => selectsWithoutValues,
146
158
  storageConfigKeys: () => storageConfigKeys,
@@ -149,12 +161,14 @@ __export(exports_src, {
149
161
  transformConnectionToEdge: () => transformConnectionToEdge,
150
162
  transformFlowToWorkflow: () => transformFlowToWorkflow,
151
163
  transformStepToNode: () => transformStepToNode,
164
+ validateBuildQueryConfig: () => validateBuildQueryConfig,
152
165
  validateConnectorTriggerEvents: () => validateConnectorTriggerEvents,
153
166
  validateEdge: () => validateEdge,
154
167
  validateFlowData: () => validateFlowData,
155
168
  validateNode: () => validateNode,
156
169
  validateStepConfig: () => validateStepConfig,
157
170
  validateStepConfigs: () => validateStepConfigs,
171
+ validateVisualConfig: () => validateVisualConfig,
158
172
  validateWorkflow: () => validateWorkflow
159
173
  });
160
174
  module.exports = __toCommonJS(exports_src);
@@ -1183,32 +1197,51 @@ var QueryType;
1183
1197
  QueryType["Delete"] = "delete";
1184
1198
  QueryType["Custom"] = "custom";
1185
1199
  })(QueryType ||= {});
1186
- var LogicalOperator;
1187
- ((LogicalOperator) => {
1188
- LogicalOperator["And"] = "and";
1189
- LogicalOperator["Or"] = "or";
1190
- })(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"];
1191
1237
  var SortDirection;
1192
1238
  ((SortDirection) => {
1193
1239
  SortDirection["Ascending"] = "asc";
1194
1240
  SortDirection["Descending"] = "desc";
1195
1241
  })(SortDirection ||= {});
1196
- var JoinType;
1197
- ((JoinType) => {
1198
- JoinType["Inner"] = "inner";
1199
- JoinType["Left"] = "left";
1200
- JoinType["Right"] = "right";
1201
- JoinType["Full"] = "full";
1202
- })(JoinType ||= {});
1203
- var AggregateFunction;
1204
- ((AggregateFunction) => {
1205
- AggregateFunction["Count"] = "count";
1206
- AggregateFunction["Sum"] = "sum";
1207
- AggregateFunction["Average"] = "avg";
1208
- AggregateFunction["Min"] = "min";
1209
- AggregateFunction["Max"] = "max";
1210
- AggregateFunction["CountDistinct"] = "count_distinct";
1211
- })(AggregateFunction ||= {});
1242
+ // src/catalog/isOneOf.ts
1243
+ var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
1244
+
1212
1245
  // src/catalog/general/query/config.ts
1213
1246
  var BuildQueryConfigDefaults = {
1214
1247
  mode: "visual" /* Visual */,
@@ -1218,160 +1251,262 @@ var BuildQueryConfigDefaults = {
1218
1251
  visualConfig: {
1219
1252
  tables: [],
1220
1253
  fields: [],
1221
- filters: undefined,
1254
+ conditions: [],
1222
1255
  joins: [],
1223
1256
  groupBy: [],
1257
+ aggregates: [],
1224
1258
  sort: [],
1225
- limit: 10,
1226
- offset: 0
1227
- },
1228
- sqlConfig: {
1229
- sql: "",
1230
- parameters: {}
1259
+ limit: 10
1231
1260
  }
1232
1261
  };
1233
- var validateBuildQueryConfig = (config) => {
1234
- const errors = [];
1235
- const warnings = [];
1236
- if (!config.connectorId) {
1237
- errors.push({
1238
- field: "connectorId",
1239
- message: "Please select a connector",
1240
- severity: "error"
1241
- });
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' }`)];
1242
1292
  }
1243
- if (!config.mode) {
1244
- errors.push({
1245
- field: "mode",
1246
- message: "Query mode is required",
1247
- severity: "error"
1248
- });
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)}")`));
1249
1297
  }
1250
- if (!config.queryType) {
1251
- errors.push({
1252
- field: "queryType",
1253
- message: "Query type is required",
1254
- severity: "error"
1255
- });
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)}")`));
1256
1300
  }
1257
- if (config.mode === "visual" /* Visual */) {
1258
- const visualErrors = validateVisualQueryConfig(config.visualConfig);
1259
- errors.push(...visualErrors);
1260
- } else if (config.mode === "sql" /* SQL */) {
1261
- const sqlErrors = validateSQLQueryConfig(config.sqlConfig);
1262
- 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
+ }
1263
1306
  }
1264
- if (config.timeout !== undefined) {
1265
- if (config.timeout < 1000) {
1266
- errors.push({
1267
- field: "timeout",
1268
- message: "Timeout must be at least 1000ms",
1269
- severity: "error"
1270
- });
1271
- } else if (config.timeout > 300000) {
1272
- warnings.push({
1273
- field: "timeout",
1274
- message: "Timeout is very high (>5 minutes)",
1275
- severity: "warning"
1276
- });
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));
1277
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`));
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`));
1278
1335
  }
1279
- if (config.maxRows !== undefined) {
1280
- if (config.maxRows < 1) {
1281
- errors.push({
1282
- field: "maxRows",
1283
- message: "Max rows must be at least 1",
1284
- severity: "error"
1285
- });
1286
- } else if (config.maxRows > 1e4) {
1287
- warnings.push({
1288
- field: "maxRows",
1289
- message: "Max rows is very high (>10,000). This may impact performance.",
1290
- severity: "warning"
1291
- });
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`));
1292
1346
  }
1293
1347
  }
1294
- return {
1295
- valid: errors.length === 0,
1296
- errors,
1297
- warnings
1298
- };
1299
- };
1300
- 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;
1301
1467
  const errors = [];
1302
- if (!config) {
1303
- errors.push({
1304
- field: "visualConfig",
1305
- message: "Visual query configuration is required",
1306
- severity: "error"
1307
- });
1308
- return errors;
1468
+ const warnings = [];
1469
+ if (!c.connectorId) {
1470
+ errors.push(err("connectorId", "Please select a connector (connectorId)"));
1309
1471
  }
1310
- if (!config.tables || config.tables.length === 0) {
1311
- errors.push({
1312
- field: "visualConfig.tables",
1313
- message: "At least one table must be selected",
1314
- severity: "error"
1315
- });
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'"));
1316
1478
  }
1317
- if (!config.fields || config.fields.length === 0) {
1318
- errors.push({
1319
- field: "visualConfig.fields",
1320
- message: "At least one field must be selected",
1321
- severity: "error"
1322
- });
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."));
1323
1484
  }
1324
- if (config.tables && config.tables.length > 1) {
1325
- if (!config.joins || config.joins.length === 0) {
1326
- errors.push({
1327
- field: "visualConfig.joins",
1328
- message: "Joins are required when selecting from multiple tables",
1329
- severity: "error"
1330
- });
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));
1331
1491
  }
1332
1492
  }
1333
- const hasAggregates = config.fields?.some((field) => field.aggregate);
1334
- const hasNonAggregates = config.fields?.some((field) => !field.aggregate);
1335
- if (hasAggregates && hasNonAggregates) {
1336
- if (!config.groupBy || config.groupBy.length === 0) {
1337
- errors.push({
1338
- field: "visualConfig.groupBy",
1339
- message: "Group by is required when mixing aggregate and non-aggregate fields",
1340
- severity: "error"
1341
- });
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" });
1342
1499
  }
1343
1500
  }
1344
- return errors;
1345
- };
1346
- var validateSQLQueryConfig = (config) => {
1347
- const errors = [];
1348
- if (!config) {
1349
- errors.push({
1350
- field: "sqlConfig",
1351
- message: "SQL query configuration is required",
1352
- severity: "error"
1353
- });
1354
- return errors;
1355
- }
1356
- if (!config.sql || config.sql.trim().length === 0) {
1357
- errors.push({
1358
- field: "sqlConfig.sql",
1359
- message: "SQL query is required",
1360
- severity: "error"
1361
- });
1362
- } else {
1363
- const sql = config.sql.trim().toLowerCase();
1364
- const dangerousKeywords = ["drop", "truncate", "alter", "create", "grant", "revoke"];
1365
- const hasDangerousKeyword = dangerousKeywords.some((keyword) => sql.includes(keyword + " "));
1366
- if (hasDangerousKeyword) {
1367
- errors.push({
1368
- field: "sqlConfig.sql",
1369
- message: "SQL query contains potentially dangerous operations (DROP, TRUNCATE, ALTER, etc.)",
1370
- severity: "error"
1371
- });
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" });
1372
1507
  }
1373
1508
  }
1374
- return errors;
1509
+ return { valid: errors.length === 0, errors, warnings };
1375
1510
  };
1376
1511
  function isQueryableConnector(connector) {
1377
1512
  if (connector.isQueryable !== undefined) {
@@ -1403,17 +1538,26 @@ var BuildQueryContractSchema = {
1403
1538
  mode: {
1404
1539
  type: "select",
1405
1540
  required: true,
1406
- options: ["visual", "sql"],
1407
- 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."
1408
1543
  },
1409
1544
  queryType: {
1410
1545
  type: "select",
1411
1546
  required: true,
1412
- options: ["select", "insert", "update", "delete", "custom"],
1413
- 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."
1414
1560
  },
1415
- visualConfig: { type: "object", required: false },
1416
- sqlConfig: { type: "object", required: false },
1417
1561
  timeout: { type: "number", required: false },
1418
1562
  maxRows: { type: "number", required: false }
1419
1563
  };
@@ -1422,11 +1566,11 @@ var BuildQueryContractSchema = {
1422
1566
  var queryDefinition = {
1423
1567
  id: "query",
1424
1568
  name: "Query",
1425
- description: "Execute queries against connected databases, APIs, and CRMs",
1426
- 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.",
1427
1571
  category: "database" /* Database */,
1428
1572
  type: "query" /* Query */,
1429
- 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"],
1430
1574
  status: "stable" /* Stable */,
1431
1575
  icon: "QueryQueue",
1432
1576
  color: "#0043ce",
@@ -1444,17 +1588,12 @@ var queryDefinition = {
1444
1588
  key: "mode",
1445
1589
  label: "Query Mode",
1446
1590
  type: "select" /* Select */,
1447
- description: "Choose between visual query builder or SQL editor",
1591
+ description: "Always 'visual' — the query is built with the visual builder",
1448
1592
  options: [
1449
1593
  {
1450
- label: "Visual Builder (No-Code)",
1594
+ label: "Visual Builder",
1451
1595
  value: "visual" /* Visual */,
1452
- description: "Build queries visually without writing code"
1453
- },
1454
- {
1455
- label: "SQL Editor (Advanced)",
1456
- value: "sql" /* SQL */,
1457
- description: "Write raw SQL queries with syntax highlighting"
1596
+ description: "Pick the table, fields and filters — no code needed"
1458
1597
  }
1459
1598
  ],
1460
1599
  defaultValue: "visual" /* Visual */,
@@ -1502,7 +1641,7 @@ var queryDefinition = {
1502
1641
  key: "visualConfig.fields",
1503
1642
  label: "Select Fields",
1504
1643
  type: "array" /* Array */,
1505
- 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.',
1506
1645
  validation: { required: true },
1507
1646
  conditional: [
1508
1647
  {
@@ -1545,39 +1684,6 @@ var queryDefinition = {
1545
1684
  }
1546
1685
  ]
1547
1686
  },
1548
- {
1549
- key: "sqlConfig.sql",
1550
- label: "SQL Query",
1551
- type: "code" /* Code */,
1552
- language: "sql",
1553
- description: "Write your SQL query here",
1554
- placeholder: "SELECT * FROM users WHERE status = 'active' LIMIT 100",
1555
- validation: { required: true },
1556
- conditional: [
1557
- {
1558
- field: "mode",
1559
- operator: "==",
1560
- value: "sql" /* SQL */,
1561
- action: "show"
1562
- }
1563
- ]
1564
- },
1565
- {
1566
- key: "sqlConfig.parameters",
1567
- label: "Query Parameters",
1568
- type: "json" /* JSON */,
1569
- description: 'Parameters for parameterized queries (e.g., {"userId": 123})',
1570
- validation: { required: false },
1571
- advanced: true,
1572
- conditional: [
1573
- {
1574
- field: "mode",
1575
- operator: "==",
1576
- value: "sql" /* SQL */,
1577
- action: "show"
1578
- }
1579
- ]
1580
- },
1581
1687
  {
1582
1688
  key: "timeout",
1583
1689
  label: "Query Timeout (ms)",
@@ -1591,7 +1697,7 @@ var queryDefinition = {
1591
1697
  key: "maxRows",
1592
1698
  label: "Max Rows",
1593
1699
  type: "number" /* Number */,
1594
- 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.",
1595
1701
  defaultValue: 1000,
1596
1702
  validation: { required: false, min: 1, max: 1e4 },
1597
1703
  advanced: true
@@ -1607,7 +1713,7 @@ var queryDefinition = {
1607
1713
  {
1608
1714
  id: "query_mode",
1609
1715
  label: "Query Configuration",
1610
- description: "Configure how you want to build your query",
1716
+ description: "The query is built with the visual builder",
1611
1717
  fields: ["mode", "queryType"]
1612
1718
  },
1613
1719
  {
@@ -1616,12 +1722,6 @@ var queryDefinition = {
1616
1722
  description: "Build your query visually without writing code",
1617
1723
  fields: ["visualConfig.tables", "visualConfig.fields", "visualConfig.limit"]
1618
1724
  },
1619
- {
1620
- id: "sql_editor",
1621
- label: "SQL Editor",
1622
- description: "Write raw SQL queries",
1623
- fields: ["sqlConfig.sql", "sqlConfig.parameters"]
1624
- },
1625
1725
  {
1626
1726
  id: "settings",
1627
1727
  label: "Advanced Settings",
@@ -1661,7 +1761,7 @@ var queryDefinition = {
1661
1761
  {
1662
1762
  key: "generatedSQL",
1663
1763
  type: "string" /* String */,
1664
- description: "The SQL query that was executed (for visual builder mode)",
1764
+ description: "The query that ran, compiled from the visual builder",
1665
1765
  example: "SELECT id, name, email FROM users WHERE status = 'active' LIMIT 100"
1666
1766
  }
1667
1767
  ],
@@ -2856,6 +2956,10 @@ function isSatisfiedOutOfBand(actionId, fieldKey, config) {
2856
2956
  return true;
2857
2957
  return Array.isArray(config.blocks) && config.blocks.length > 0;
2858
2958
  }
2959
+ if (actionId === "query" && fieldKey === "visualConfig.fields")
2960
+ return queryFieldsSatisfied(config);
2961
+ if (actionId === "query" && fieldKey === "visualConfig.tables")
2962
+ return queryTablesSatisfied(config);
2859
2963
  return false;
2860
2964
  }
2861
2965
  var MESSAGE_BODY_ACTIONS = new Set([
@@ -3280,6 +3384,9 @@ var CONDITION_OPERATORS = [
3280
3384
  { id: "is_not_null", label: "Is Not Null", needsValue: false },
3281
3385
  { id: "is_empty", label: "Is Empty", needsValue: false }
3282
3386
  ];
3387
+ function isMissingValue(value) {
3388
+ return value === undefined || value === null || value === "";
3389
+ }
3283
3390
  var ConditionConfig = {
3284
3391
  defaults: {
3285
3392
  mode: "visual",
@@ -3313,7 +3420,7 @@ var ConditionConfig = {
3313
3420
  if (!f.operator)
3314
3421
  errors.push(`Filter ${i + 1}: Operator is required`);
3315
3422
  const opDef = CONDITION_OPERATORS.find((o) => o.id === f.operator);
3316
- if (opDef?.needsValue && !f.value) {
3423
+ if (opDef?.needsValue && isMissingValue(f.value)) {
3317
3424
  errors.push(`Filter ${i + 1}: Value is required`);
3318
3425
  }
3319
3426
  });
@@ -3750,9 +3857,6 @@ var EmailConfig = {
3750
3857
  }
3751
3858
  }
3752
3859
  };
3753
- // src/catalog/isOneOf.ts
3754
- var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3755
-
3756
3860
  // src/catalog/general/transform/config.ts
3757
3861
  var TRANSFORM_MODES = ["visual", "expression"];
3758
3862
  var TRANSFORM_OPERATIONS = [