@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.js CHANGED
@@ -1022,32 +1022,51 @@ var QueryType;
1022
1022
  QueryType["Delete"] = "delete";
1023
1023
  QueryType["Custom"] = "custom";
1024
1024
  })(QueryType ||= {});
1025
- var LogicalOperator;
1026
- ((LogicalOperator) => {
1027
- LogicalOperator["And"] = "and";
1028
- LogicalOperator["Or"] = "or";
1029
- })(LogicalOperator ||= {});
1025
+ var QUERY_MODES = ["visual" /* Visual */];
1026
+ var QUERY_TYPES = ["select" /* Select */];
1027
+ var QUERY_COMPARISON_OPERATORS = [
1028
+ "=",
1029
+ "!=",
1030
+ ">",
1031
+ "<",
1032
+ ">=",
1033
+ "<=",
1034
+ "equals",
1035
+ "notEquals",
1036
+ "greaterThan",
1037
+ "lessThan",
1038
+ "greaterOrEqual",
1039
+ "lessOrEqual"
1040
+ ];
1041
+ var QUERY_RELATIVE_DATE_OPERATORS = ["inRelative", "notInRelative"];
1042
+ var QUERY_CONDITION_OPERATORS = [
1043
+ ...QUERY_COMPARISON_OPERATORS,
1044
+ "contains",
1045
+ "startsWith",
1046
+ "endsWith",
1047
+ "like",
1048
+ "notLike",
1049
+ "in",
1050
+ "notIn",
1051
+ "between",
1052
+ "isNull",
1053
+ "isNotNull",
1054
+ ...QUERY_RELATIVE_DATE_OPERATORS
1055
+ ];
1056
+ var RELATIVE_DATE_DIRECTIONS = ["last", "next", "this"];
1057
+ var RELATIVE_DATE_UNITS = ["day", "week", "month", "quarter", "year"];
1058
+ var RELATIVE_DATE_MAX_AMOUNT = 3650;
1059
+ var MAX_CONDITION_DEPTH = 3;
1060
+ var isConditionGroup = (c) => !!c && typeof c === "object" && c.type === "group";
1061
+ var QUERY_AGGREGATE_FUNCTIONS = ["COUNT", "COUNT_DISTINCT", "SUM", "AVG", "MIN", "MAX"];
1030
1062
  var SortDirection;
1031
1063
  ((SortDirection) => {
1032
1064
  SortDirection["Ascending"] = "asc";
1033
1065
  SortDirection["Descending"] = "desc";
1034
1066
  })(SortDirection ||= {});
1035
- var JoinType;
1036
- ((JoinType) => {
1037
- JoinType["Inner"] = "inner";
1038
- JoinType["Left"] = "left";
1039
- JoinType["Right"] = "right";
1040
- JoinType["Full"] = "full";
1041
- })(JoinType ||= {});
1042
- var AggregateFunction;
1043
- ((AggregateFunction) => {
1044
- AggregateFunction["Count"] = "count";
1045
- AggregateFunction["Sum"] = "sum";
1046
- AggregateFunction["Average"] = "avg";
1047
- AggregateFunction["Min"] = "min";
1048
- AggregateFunction["Max"] = "max";
1049
- AggregateFunction["CountDistinct"] = "count_distinct";
1050
- })(AggregateFunction ||= {});
1067
+ // src/catalog/isOneOf.ts
1068
+ var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
1069
+
1051
1070
  // src/catalog/general/query/config.ts
1052
1071
  var BuildQueryConfigDefaults = {
1053
1072
  mode: "visual" /* Visual */,
@@ -1057,160 +1076,262 @@ var BuildQueryConfigDefaults = {
1057
1076
  visualConfig: {
1058
1077
  tables: [],
1059
1078
  fields: [],
1060
- filters: undefined,
1079
+ conditions: [],
1061
1080
  joins: [],
1062
1081
  groupBy: [],
1082
+ aggregates: [],
1063
1083
  sort: [],
1064
- limit: 10,
1065
- offset: 0
1066
- },
1067
- sqlConfig: {
1068
- sql: "",
1069
- parameters: {}
1084
+ limit: 10
1070
1085
  }
1071
1086
  };
1072
- var validateBuildQueryConfig = (config) => {
1073
- const errors = [];
1074
- const warnings = [];
1075
- if (!config.connectorId) {
1076
- errors.push({
1077
- field: "connectorId",
1078
- message: "Please select a connector",
1079
- severity: "error"
1080
- });
1087
+ 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.";
1088
+ 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.";
1089
+ var err = (field, message) => ({ field, message, severity: "error" });
1090
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
1091
+ var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
1092
+ var hasValue = (v) => v !== undefined && v !== null && !(typeof v === "string" && v.trim() === "") && !(Array.isArray(v) && v.length === 0);
1093
+ var arr = (v) => Array.isArray(v) ? v : [];
1094
+ function queryFieldsSatisfied(config) {
1095
+ const vc = isRecord(config?.visualConfig) ? config.visualConfig : undefined;
1096
+ if (!vc)
1097
+ return false;
1098
+ if (vc.selectAll === true)
1099
+ return true;
1100
+ if (arr(vc.fields).length > 0)
1101
+ return true;
1102
+ if (arr(vc.aggregates).length > 0 || arr(vc.groupBy).length > 0)
1103
+ return true;
1104
+ const sql = isRecord(config?.sqlConfig) ? config.sqlConfig.sql : undefined;
1105
+ return arr(vc.tables).length > 0 && nonEmptyString(sql);
1106
+ }
1107
+ function queryTablesSatisfied(config) {
1108
+ const vc = isRecord(config?.visualConfig) ? config.visualConfig : undefined;
1109
+ if (!vc)
1110
+ return false;
1111
+ return vc.selectAll === true || arr(vc.tables).length > 0;
1112
+ }
1113
+ var OPERATOR_LIST = QUERY_CONDITION_OPERATORS.join(", ");
1114
+ function validateRelative(path, relative) {
1115
+ if (!isRecord(relative)) {
1116
+ return [err(`${path}.relative`, `${path}: a relative-date condition needs relative, e.g. { direction: 'last', amount: 30, unit: 'day' }`)];
1081
1117
  }
1082
- if (!config.mode) {
1083
- errors.push({
1084
- field: "mode",
1085
- message: "Query mode is required",
1086
- severity: "error"
1087
- });
1118
+ const issues = [];
1119
+ const { direction, unit, amount, includeCurrent } = relative;
1120
+ if (!isOneOf(RELATIVE_DATE_DIRECTIONS, direction)) {
1121
+ issues.push(err(`${path}.relative.direction`, `${path}.relative.direction must be one of: ${RELATIVE_DATE_DIRECTIONS.join(", ")} (got "${String(direction)}")`));
1088
1122
  }
1089
- if (!config.queryType) {
1090
- errors.push({
1091
- field: "queryType",
1092
- message: "Query type is required",
1093
- severity: "error"
1094
- });
1123
+ if (!isOneOf(RELATIVE_DATE_UNITS, unit)) {
1124
+ issues.push(err(`${path}.relative.unit`, `${path}.relative.unit must be one of: ${RELATIVE_DATE_UNITS.join(", ")} (got "${String(unit)}")`));
1095
1125
  }
1096
- if (config.mode === "visual" /* Visual */) {
1097
- const visualErrors = validateVisualQueryConfig(config.visualConfig);
1098
- errors.push(...visualErrors);
1099
- } else if (config.mode === "sql" /* SQL */) {
1100
- const sqlErrors = validateSQLQueryConfig(config.sqlConfig);
1101
- errors.push(...sqlErrors);
1126
+ if (direction === "last" || direction === "next") {
1127
+ const ok = typeof amount === "number" && Number.isInteger(amount) && amount >= 1 && amount <= RELATIVE_DATE_MAX_AMOUNT;
1128
+ if (!ok) {
1129
+ 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' })`));
1130
+ }
1102
1131
  }
1103
- if (config.timeout !== undefined) {
1104
- if (config.timeout < 1000) {
1105
- errors.push({
1106
- field: "timeout",
1107
- message: "Timeout must be at least 1000ms",
1108
- severity: "error"
1109
- });
1110
- } else if (config.timeout > 300000) {
1111
- warnings.push({
1112
- field: "timeout",
1113
- message: "Timeout is very high (>5 minutes)",
1114
- severity: "warning"
1115
- });
1132
+ if (includeCurrent !== undefined && typeof includeCurrent !== "boolean") {
1133
+ issues.push(err(`${path}.relative.includeCurrent`, `${path}.relative.includeCurrent must be true or false`));
1134
+ }
1135
+ return issues;
1136
+ }
1137
+ function validateRow(path, row, inHaving) {
1138
+ const issues = [];
1139
+ if (!nonEmptyString(row.field)) {
1140
+ issues.push(err(`${path}.field`, `${path}: choose the field to compare (field is empty)`));
1141
+ }
1142
+ if (!isOneOf(QUERY_CONDITION_OPERATORS, row.operator)) {
1143
+ issues.push(err(`${path}.operator`, `${path}.operator must be one of: ${OPERATOR_LIST} (got "${String(row.operator)}")`));
1144
+ return issues;
1145
+ }
1146
+ const op = row.operator;
1147
+ const isRelative = isOneOf(QUERY_RELATIVE_DATE_OPERATORS, op);
1148
+ const comparesField = row.valueField !== undefined && row.valueField !== null && row.valueField !== "";
1149
+ if (isRelative) {
1150
+ if (inHaving) {
1151
+ issues.push(err(`${path}.operator`, `${path}: '${op}' filters dates on rows — put it in conditions, not having`));
1152
+ } else {
1153
+ issues.push(...validateRelative(path, row.relative));
1116
1154
  }
1155
+ if (hasValue(row.value) || comparesField) {
1156
+ issues.push(err(path, `${path}: a relative-date condition takes its window from relative — remove value and valueField`));
1157
+ }
1158
+ } else if (row.relative !== undefined) {
1159
+ issues.push(err(`${path}.relative`, `${path}: relative is only used with 'inRelative' or 'notInRelative' — change the operator or remove relative`));
1117
1160
  }
1118
- if (config.maxRows !== undefined) {
1119
- if (config.maxRows < 1) {
1120
- errors.push({
1121
- field: "maxRows",
1122
- message: "Max rows must be at least 1",
1123
- severity: "error"
1124
- });
1125
- } else if (config.maxRows > 1e4) {
1126
- warnings.push({
1127
- field: "maxRows",
1128
- message: "Max rows is very high (>10,000). This may impact performance.",
1129
- severity: "warning"
1130
- });
1161
+ if (comparesField) {
1162
+ if (inHaving) {
1163
+ issues.push(err(`${path}.valueField`, `${path}: having compares with a value — use value, not valueField`));
1164
+ } else if (!nonEmptyString(row.valueField)) {
1165
+ issues.push(err(`${path}.valueField`, `${path}.valueField must name the field to compare with`));
1166
+ } else if (!isOneOf(QUERY_COMPARISON_OPERATORS, op)) {
1167
+ issues.push(err(`${path}.valueField`, `${path}: comparing with another field works with =, !=, >, <, >=, <= only (got "${op}")`));
1168
+ }
1169
+ if (hasValue(row.value)) {
1170
+ issues.push(err(path, `${path}: set either value (compare with a value) or valueField (compare with another field), not both`));
1131
1171
  }
1132
1172
  }
1133
- return {
1134
- valid: errors.length === 0,
1135
- errors,
1136
- warnings
1137
- };
1138
- };
1139
- var validateVisualQueryConfig = (config) => {
1173
+ if (row.joinOperator !== undefined && !(typeof row.joinOperator === "string" && /^(and|or)$/i.test(row.joinOperator))) {
1174
+ issues.push(err(`${path}.joinOperator`, `${path}.joinOperator must be AND or OR (got "${String(row.joinOperator)}")`));
1175
+ }
1176
+ return issues;
1177
+ }
1178
+ function validateConditionList(path, list, depth) {
1179
+ if (list === undefined || list === null)
1180
+ return [];
1181
+ if (!Array.isArray(list))
1182
+ return [err(path, `${path} must be a list of conditions`)];
1183
+ const issues = [];
1184
+ list.forEach((item, i) => {
1185
+ const at = `${path}[${i}]`;
1186
+ if (!isRecord(item)) {
1187
+ issues.push(err(at, `${at} must be a condition ({ field, operator, value }) or a group ({ type: 'group', conditions })`));
1188
+ return;
1189
+ }
1190
+ if (isConditionGroup(item)) {
1191
+ if (depth + 1 > MAX_CONDITION_DEPTH) {
1192
+ 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`));
1193
+ return;
1194
+ }
1195
+ if (item.joinOperator !== undefined && !(typeof item.joinOperator === "string" && /^(and|or)$/i.test(item.joinOperator))) {
1196
+ issues.push(err(`${at}.joinOperator`, `${at}.joinOperator must be AND or OR (got "${String(item.joinOperator)}")`));
1197
+ }
1198
+ if (!Array.isArray(item.conditions) || item.conditions.length === 0) {
1199
+ issues.push(err(`${at}.conditions`, `${at}: a group needs at least one condition — add one or remove the group`));
1200
+ return;
1201
+ }
1202
+ issues.push(...validateConditionList(`${at}.conditions`, item.conditions, depth + 1));
1203
+ return;
1204
+ }
1205
+ issues.push(...validateRow(at, item, false));
1206
+ });
1207
+ return issues;
1208
+ }
1209
+ function validateAggregates(vc) {
1210
+ const list = vc.aggregates;
1211
+ if (list === undefined || list === null)
1212
+ return [];
1213
+ if (!Array.isArray(list))
1214
+ return [err("visualConfig.aggregates", "visualConfig.aggregates must be a list of { fn, field, alias }")];
1215
+ const issues = [];
1216
+ list.forEach((a, i) => {
1217
+ const at = `visualConfig.aggregates[${i}]`;
1218
+ if (!isRecord(a)) {
1219
+ issues.push(err(at, `${at} must be { fn, field, alias }`));
1220
+ return;
1221
+ }
1222
+ const fn = typeof a.fn === "string" ? a.fn.toUpperCase() : a.fn;
1223
+ if (!isOneOf(QUERY_AGGREGATE_FUNCTIONS, fn)) {
1224
+ issues.push(err(`${at}.fn`, `${at}.fn must be one of: ${QUERY_AGGREGATE_FUNCTIONS.join(", ")} (got "${String(a.fn)}")`));
1225
+ return;
1226
+ }
1227
+ const allRows = !nonEmptyString(a.field) || a.field.trim() === "*";
1228
+ if (allRows && fn !== "COUNT") {
1229
+ issues.push(err(`${at}.field`, `${at}: ${fn} needs a field to work on — set field to a column name`));
1230
+ }
1231
+ if (a.alias !== undefined && a.alias !== null && !nonEmptyString(a.alias)) {
1232
+ issues.push(err(`${at}.alias`, `${at}.alias must be a name for the result column, or left out`));
1233
+ }
1234
+ });
1235
+ return issues;
1236
+ }
1237
+ function validateHaving(vc) {
1238
+ const list = vc.having;
1239
+ if (list === undefined || list === null || Array.isArray(list) && list.length === 0)
1240
+ return [];
1241
+ if (!Array.isArray(list))
1242
+ return [err("visualConfig.having", "visualConfig.having must be a list of conditions")];
1243
+ const aggregates = arr(vc.aggregates).filter(isRecord);
1244
+ const groupBy = arr(vc.groupBy).filter(nonEmptyString);
1245
+ if (aggregates.length === 0 && groupBy.length === 0) {
1246
+ return [err("visualConfig.having", "visualConfig.having filters grouped results, so it needs aggregates or groupBy — add them, or move these filters to conditions")];
1247
+ }
1248
+ const names = new Set([
1249
+ ...groupBy,
1250
+ ...aggregates.map((a) => a.alias).filter(nonEmptyString)
1251
+ ]);
1252
+ const issues = [];
1253
+ list.forEach((row, i) => {
1254
+ const at = `visualConfig.having[${i}]`;
1255
+ if (!isRecord(row) || isConditionGroup(row)) {
1256
+ issues.push(err(at, `${at} must be a single condition ({ field, operator, value }); groups are not used in having`));
1257
+ return;
1258
+ }
1259
+ issues.push(...validateRow(at, row, true));
1260
+ if (nonEmptyString(row.field) && !names.has(row.field)) {
1261
+ const known = [...names];
1262
+ 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)")));
1263
+ }
1264
+ });
1265
+ return issues;
1266
+ }
1267
+ function validateVisualConfig(config) {
1268
+ const vc = config.visualConfig;
1269
+ if (!isRecord(vc)) {
1270
+ return [err("visualConfig", "visualConfig is required — describe the query with tables (what to read), fields (which columns) and optional conditions")];
1271
+ }
1272
+ const issues = [];
1273
+ if (!queryTablesSatisfied(config)) {
1274
+ issues.push(err("visualConfig.tables", "At least one table must be selected (visualConfig.tables)"));
1275
+ }
1276
+ if (!queryFieldsSatisfied(config)) {
1277
+ issues.push(err("visualConfig.fields", "At least one field must be selected (visualConfig.fields), or add aggregates / groupBy for a summary query"));
1278
+ }
1279
+ if (arr(vc.tables).length > 1 && arr(vc.joins).length === 0) {
1280
+ issues.push(err("visualConfig.joins", "Joins are required when selecting from multiple tables (visualConfig.joins)"));
1281
+ }
1282
+ if (vc.distinct !== undefined && typeof vc.distinct !== "boolean") {
1283
+ issues.push(err("visualConfig.distinct", "visualConfig.distinct must be true or false"));
1284
+ }
1285
+ issues.push(...validateConditionList("visualConfig.conditions", vc.conditions, 0));
1286
+ issues.push(...validateAggregates(vc));
1287
+ issues.push(...validateHaving(vc));
1288
+ return issues;
1289
+ }
1290
+ var validateBuildQueryConfig = (config) => {
1291
+ const c = config;
1140
1292
  const errors = [];
1141
- if (!config) {
1142
- errors.push({
1143
- field: "visualConfig",
1144
- message: "Visual query configuration is required",
1145
- severity: "error"
1146
- });
1147
- return errors;
1293
+ const warnings = [];
1294
+ if (!c.connectorId) {
1295
+ errors.push(err("connectorId", "Please select a connector (connectorId)"));
1148
1296
  }
1149
- if (!config.tables || config.tables.length === 0) {
1150
- errors.push({
1151
- field: "visualConfig.tables",
1152
- message: "At least one table must be selected",
1153
- severity: "error"
1154
- });
1297
+ const mode = c.mode;
1298
+ const visual = mode === undefined || mode === null || mode === "" || isOneOf(QUERY_MODES, mode);
1299
+ if (!visual) {
1300
+ errors.push(err("mode", `Mode must be one of: ${QUERY_MODES.join(", ")} (got "${String(mode)}"). ${QUERY_VISUAL_ONLY_MESSAGE}`));
1301
+ } else if (!mode) {
1302
+ errors.push(err("mode", "Query mode is required — set mode to 'visual'"));
1155
1303
  }
1156
- if (!config.fields || config.fields.length === 0) {
1157
- errors.push({
1158
- field: "visualConfig.fields",
1159
- message: "At least one field must be selected",
1160
- severity: "error"
1161
- });
1304
+ const queryType = c.queryType;
1305
+ if (!queryType) {
1306
+ errors.push(err("queryType", "Query type is required — set queryType to 'select'"));
1307
+ } else if (!isOneOf(QUERY_TYPES, queryType)) {
1308
+ 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."));
1162
1309
  }
1163
- if (config.tables && config.tables.length > 1) {
1164
- if (!config.joins || config.joins.length === 0) {
1165
- errors.push({
1166
- field: "visualConfig.joins",
1167
- message: "Joins are required when selecting from multiple tables",
1168
- severity: "error"
1169
- });
1310
+ if (visual) {
1311
+ const sql = isRecord(c.sqlConfig) ? c.sqlConfig.sql : undefined;
1312
+ if (nonEmptyString(sql) && !isRecord(c.visualConfig)) {
1313
+ errors.push(err("sqlConfig.sql", QUERY_AUTHORED_SQL_MESSAGE));
1314
+ } else {
1315
+ errors.push(...validateVisualConfig(c));
1170
1316
  }
1171
1317
  }
1172
- const hasAggregates = config.fields?.some((field) => field.aggregate);
1173
- const hasNonAggregates = config.fields?.some((field) => !field.aggregate);
1174
- if (hasAggregates && hasNonAggregates) {
1175
- if (!config.groupBy || config.groupBy.length === 0) {
1176
- errors.push({
1177
- field: "visualConfig.groupBy",
1178
- message: "Group by is required when mixing aggregate and non-aggregate fields",
1179
- severity: "error"
1180
- });
1318
+ const timeout = c.timeout;
1319
+ if (typeof timeout === "number") {
1320
+ if (timeout < 1000) {
1321
+ errors.push(err("timeout", "Timeout must be at least 1000ms"));
1322
+ } else if (timeout > 300000) {
1323
+ warnings.push({ field: "timeout", message: "Timeout is very high (>5 minutes)", severity: "warning" });
1181
1324
  }
1182
1325
  }
1183
- return errors;
1184
- };
1185
- var validateSQLQueryConfig = (config) => {
1186
- const errors = [];
1187
- if (!config) {
1188
- errors.push({
1189
- field: "sqlConfig",
1190
- message: "SQL query configuration is required",
1191
- severity: "error"
1192
- });
1193
- return errors;
1194
- }
1195
- if (!config.sql || config.sql.trim().length === 0) {
1196
- errors.push({
1197
- field: "sqlConfig.sql",
1198
- message: "SQL query is required",
1199
- severity: "error"
1200
- });
1201
- } else {
1202
- const sql = config.sql.trim().toLowerCase();
1203
- const dangerousKeywords = ["drop", "truncate", "alter", "create", "grant", "revoke"];
1204
- const hasDangerousKeyword = dangerousKeywords.some((keyword) => sql.includes(keyword + " "));
1205
- if (hasDangerousKeyword) {
1206
- errors.push({
1207
- field: "sqlConfig.sql",
1208
- message: "SQL query contains potentially dangerous operations (DROP, TRUNCATE, ALTER, etc.)",
1209
- severity: "error"
1210
- });
1326
+ const maxRows = c.maxRows;
1327
+ if (typeof maxRows === "number") {
1328
+ if (maxRows < 1) {
1329
+ errors.push(err("maxRows", "Max rows must be at least 1"));
1330
+ } else if (maxRows > 1e4) {
1331
+ warnings.push({ field: "maxRows", message: "Max rows is very high (>10,000). This may impact performance.", severity: "warning" });
1211
1332
  }
1212
1333
  }
1213
- return errors;
1334
+ return { valid: errors.length === 0, errors, warnings };
1214
1335
  };
1215
1336
  function isQueryableConnector(connector) {
1216
1337
  if (connector.isQueryable !== undefined) {
@@ -1242,17 +1363,26 @@ var BuildQueryContractSchema = {
1242
1363
  mode: {
1243
1364
  type: "select",
1244
1365
  required: true,
1245
- options: ["visual", "sql"],
1246
- description: "visual: the query is built from visualConfig; sql: the query is the text in sqlConfig.sql"
1366
+ options: QUERY_MODES,
1367
+ description: "Always 'visual': the Query step is built with the visual builder and described in visualConfig. Hand-written SQL is not accepted."
1247
1368
  },
1248
1369
  queryType: {
1249
1370
  type: "select",
1250
1371
  required: true,
1251
- options: ["select", "insert", "update", "delete", "custom"],
1252
- description: "Only select is a read; the write types exist for the SQL mode of database connectors"
1372
+ options: QUERY_TYPES,
1373
+ description: "Always 'select': the Query step reads data. Use a Transform step to write."
1374
+ },
1375
+ visualConfig: {
1376
+ type: "object",
1377
+ required: false,
1378
+ 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)."
1379
+ },
1380
+ sqlConfig: {
1381
+ type: "object",
1382
+ required: false,
1383
+ derived: true,
1384
+ description: "Compiled from visualConfig by the editor or the server. Never write it — describe the query in visualConfig."
1253
1385
  },
1254
- visualConfig: { type: "object", required: false },
1255
- sqlConfig: { type: "object", required: false },
1256
1386
  timeout: { type: "number", required: false },
1257
1387
  maxRows: { type: "number", required: false }
1258
1388
  };
@@ -1261,11 +1391,11 @@ var BuildQueryContractSchema = {
1261
1391
  var queryDefinition = {
1262
1392
  id: "query",
1263
1393
  name: "Query",
1264
- description: "Execute queries against connected databases, APIs, and CRMs",
1265
- 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.",
1394
+ description: "Read records from a connected database, app or CRM",
1395
+ 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.",
1266
1396
  category: "database" /* Database */,
1267
1397
  type: "query" /* Query */,
1268
- tags: ["query", "database", "sql", "visual-builder", "no-code", "api", "crm", "built-in"],
1398
+ tags: ["query", "database", "visual-builder", "no-code", "filter", "api", "crm", "built-in"],
1269
1399
  status: "stable" /* Stable */,
1270
1400
  icon: "QueryQueue",
1271
1401
  color: "#0043ce",
@@ -1283,17 +1413,12 @@ var queryDefinition = {
1283
1413
  key: "mode",
1284
1414
  label: "Query Mode",
1285
1415
  type: "select" /* Select */,
1286
- description: "Choose between visual query builder or SQL editor",
1416
+ description: "Always 'visual' — the query is built with the visual builder",
1287
1417
  options: [
1288
1418
  {
1289
- label: "Visual Builder (No-Code)",
1419
+ label: "Visual Builder",
1290
1420
  value: "visual" /* Visual */,
1291
- description: "Build queries visually without writing code"
1292
- },
1293
- {
1294
- label: "SQL Editor (Advanced)",
1295
- value: "sql" /* SQL */,
1296
- description: "Write raw SQL queries with syntax highlighting"
1421
+ description: "Pick the table, fields and filters — no code needed"
1297
1422
  }
1298
1423
  ],
1299
1424
  defaultValue: "visual" /* Visual */,
@@ -1341,7 +1466,7 @@ var queryDefinition = {
1341
1466
  key: "visualConfig.fields",
1342
1467
  label: "Select Fields",
1343
1468
  type: "array" /* Array */,
1344
- description: "Choose fields to include in the query result",
1469
+ description: 'The fields to return, as "table.column". Not needed for a summary query (aggregates / groupBy) or when every table and field is selected.',
1345
1470
  validation: { required: true },
1346
1471
  conditional: [
1347
1472
  {
@@ -1384,39 +1509,6 @@ var queryDefinition = {
1384
1509
  }
1385
1510
  ]
1386
1511
  },
1387
- {
1388
- key: "sqlConfig.sql",
1389
- label: "SQL Query",
1390
- type: "code" /* Code */,
1391
- language: "sql",
1392
- description: "Write your SQL query here",
1393
- placeholder: "SELECT * FROM users WHERE status = 'active' LIMIT 100",
1394
- validation: { required: true },
1395
- conditional: [
1396
- {
1397
- field: "mode",
1398
- operator: "==",
1399
- value: "sql" /* SQL */,
1400
- action: "show"
1401
- }
1402
- ]
1403
- },
1404
- {
1405
- key: "sqlConfig.parameters",
1406
- label: "Query Parameters",
1407
- type: "json" /* JSON */,
1408
- description: 'Parameters for parameterized queries (e.g., {"userId": 123})',
1409
- validation: { required: false },
1410
- advanced: true,
1411
- conditional: [
1412
- {
1413
- field: "mode",
1414
- operator: "==",
1415
- value: "sql" /* SQL */,
1416
- action: "show"
1417
- }
1418
- ]
1419
- },
1420
1512
  {
1421
1513
  key: "timeout",
1422
1514
  label: "Query Timeout (ms)",
@@ -1430,7 +1522,7 @@ var queryDefinition = {
1430
1522
  key: "maxRows",
1431
1523
  label: "Max Rows",
1432
1524
  type: "number" /* Number */,
1433
- description: "Legacy step-level field — NOT read at run. Cap a visual Query with visualConfig.maxRows; in SQL mode write a LIMIT.",
1525
+ description: "Legacy step-level field — not read at run. To cap a run, set visualConfig.maxRows.",
1434
1526
  defaultValue: 1000,
1435
1527
  validation: { required: false, min: 1, max: 1e4 },
1436
1528
  advanced: true
@@ -1446,7 +1538,7 @@ var queryDefinition = {
1446
1538
  {
1447
1539
  id: "query_mode",
1448
1540
  label: "Query Configuration",
1449
- description: "Configure how you want to build your query",
1541
+ description: "The query is built with the visual builder",
1450
1542
  fields: ["mode", "queryType"]
1451
1543
  },
1452
1544
  {
@@ -1455,12 +1547,6 @@ var queryDefinition = {
1455
1547
  description: "Build your query visually without writing code",
1456
1548
  fields: ["visualConfig.tables", "visualConfig.fields", "visualConfig.limit"]
1457
1549
  },
1458
- {
1459
- id: "sql_editor",
1460
- label: "SQL Editor",
1461
- description: "Write raw SQL queries",
1462
- fields: ["sqlConfig.sql", "sqlConfig.parameters"]
1463
- },
1464
1550
  {
1465
1551
  id: "settings",
1466
1552
  label: "Advanced Settings",
@@ -1500,7 +1586,7 @@ var queryDefinition = {
1500
1586
  {
1501
1587
  key: "generatedSQL",
1502
1588
  type: "string" /* String */,
1503
- description: "The SQL query that was executed (for visual builder mode)",
1589
+ description: "The query that ran, compiled from the visual builder",
1504
1590
  example: "SELECT id, name, email FROM users WHERE status = 'active' LIMIT 100"
1505
1591
  }
1506
1592
  ],
@@ -2695,6 +2781,10 @@ function isSatisfiedOutOfBand(actionId, fieldKey, config) {
2695
2781
  return true;
2696
2782
  return Array.isArray(config.blocks) && config.blocks.length > 0;
2697
2783
  }
2784
+ if (actionId === "query" && fieldKey === "visualConfig.fields")
2785
+ return queryFieldsSatisfied(config);
2786
+ if (actionId === "query" && fieldKey === "visualConfig.tables")
2787
+ return queryTablesSatisfied(config);
2698
2788
  return false;
2699
2789
  }
2700
2790
  var MESSAGE_BODY_ACTIONS = new Set([
@@ -3119,6 +3209,9 @@ var CONDITION_OPERATORS = [
3119
3209
  { id: "is_not_null", label: "Is Not Null", needsValue: false },
3120
3210
  { id: "is_empty", label: "Is Empty", needsValue: false }
3121
3211
  ];
3212
+ function isMissingValue(value) {
3213
+ return value === undefined || value === null || value === "";
3214
+ }
3122
3215
  var ConditionConfig = {
3123
3216
  defaults: {
3124
3217
  mode: "visual",
@@ -3152,7 +3245,7 @@ var ConditionConfig = {
3152
3245
  if (!f.operator)
3153
3246
  errors.push(`Filter ${i + 1}: Operator is required`);
3154
3247
  const opDef = CONDITION_OPERATORS.find((o) => o.id === f.operator);
3155
- if (opDef?.needsValue && !f.value) {
3248
+ if (opDef?.needsValue && isMissingValue(f.value)) {
3156
3249
  errors.push(`Filter ${i + 1}: Value is required`);
3157
3250
  }
3158
3251
  });
@@ -3589,9 +3682,6 @@ var EmailConfig = {
3589
3682
  }
3590
3683
  }
3591
3684
  };
3592
- // src/catalog/isOneOf.ts
3593
- var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3594
-
3595
3685
  // src/catalog/general/transform/config.ts
3596
3686
  var TRANSFORM_MODES = ["visual", "expression"];
3597
3687
  var TRANSFORM_OPERATIONS = [
@@ -6009,7 +6099,6 @@ export {
6009
6099
  ActionStatus,
6010
6100
  ActionType,
6011
6101
  AggregateConfig,
6012
- AggregateFunction,
6013
6102
  AiAgentConfig,
6014
6103
  CONDITION_OPERATORS,
6015
6104
  CONFIG_CONTRACTS,
@@ -6042,19 +6131,29 @@ export {
6042
6131
  GroupConfig,
6043
6132
  IDP_EXTRACT_STRATEGIES,
6044
6133
  IdpExtractConfig,
6045
- JoinType,
6046
6134
  JsonActionConfig,
6047
- LogicalOperator,
6048
6135
  MAPPING_FUNCTIONS,
6049
6136
  MAPPING_FUNCTION_SPECS,
6137
+ MAX_CONDITION_DEPTH,
6050
6138
  MAX_STEP_CONFIG_BYTES,
6051
6139
  ManualTriggerConfig,
6052
6140
  NodeStatus,
6053
6141
  ONEDRIVE_OPERATIONS,
6054
6142
  OneDriveActionConfig,
6055
6143
  PLATFORM_CONFIG_KEYS,
6144
+ QUERY_AGGREGATE_FUNCTIONS,
6145
+ QUERY_AUTHORED_SQL_MESSAGE,
6146
+ QUERY_COMPARISON_OPERATORS,
6147
+ QUERY_CONDITION_OPERATORS,
6148
+ QUERY_MODES,
6149
+ QUERY_RELATIVE_DATE_OPERATORS,
6150
+ QUERY_TYPES,
6151
+ QUERY_VISUAL_ONLY_MESSAGE,
6056
6152
  QueryMode,
6057
6153
  QueryType,
6154
+ RELATIVE_DATE_DIRECTIONS,
6155
+ RELATIVE_DATE_MAX_AMOUNT,
6156
+ RELATIVE_DATE_UNITS,
6058
6157
  S3ActionConfig,
6059
6158
  S3_OPERATIONS,
6060
6159
  STORAGE_CONNECTOR_IDS,
@@ -6098,6 +6197,7 @@ export {
6098
6197
  getStorageProvider,
6099
6198
  groupKeyName,
6100
6199
  inferSchemaFromSample,
6200
+ isConditionGroup,
6101
6201
  isEditorPrivateKey,
6102
6202
  isEventCapableConnector,
6103
6203
  isMappingFunction,
@@ -6105,6 +6205,8 @@ export {
6105
6205
  mappingTransformFunctionName,
6106
6206
  migrateDelayConfig,
6107
6207
  queryDefinition,
6208
+ queryFieldsSatisfied,
6209
+ queryTablesSatisfied,
6108
6210
  resolveRecordSchema,
6109
6211
  selectsWithoutValues,
6110
6212
  storageConfigKeys,
@@ -6113,11 +6215,13 @@ export {
6113
6215
  transformConnectionToEdge,
6114
6216
  transformFlowToWorkflow,
6115
6217
  transformStepToNode,
6218
+ validateBuildQueryConfig,
6116
6219
  validateConnectorTriggerEvents,
6117
6220
  validateEdge,
6118
6221
  validateFlowData,
6119
6222
  validateNode,
6120
6223
  validateStepConfig,
6121
6224
  validateStepConfigs,
6225
+ validateVisualConfig,
6122
6226
  validateWorkflow
6123
6227
  };