@octaviaflow/flow-rules 0.12.0 → 0.13.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
@@ -39,9 +39,12 @@ var __export = (target, all) => {
39
39
  // src/index.ts
40
40
  var exports_src = {};
41
41
  __export(exports_src, {
42
+ AGGREGATE_FUNCTIONS: () => AGGREGATE_FUNCTIONS,
43
+ AGGREGATE_FUNCTION_DESCRIPTIONS: () => AGGREGATE_FUNCTION_DESCRIPTIONS,
42
44
  ActionCategory: () => ActionCategory,
43
45
  ActionStatus: () => ActionStatus,
44
46
  ActionType: () => ActionType,
47
+ AggregateConfig: () => AggregateConfig,
45
48
  AggregateFunction: () => AggregateFunction,
46
49
  AiAgentConfig: () => AiAgentConfig,
47
50
  CONDITION_OPERATORS: () => CONDITION_OPERATORS,
@@ -113,6 +116,7 @@ __export(exports_src, {
113
116
  contractFor: () => contractFor,
114
117
  contractVocabularyDrift: () => contractVocabularyDrift,
115
118
  declaredKeys: () => declaredKeys,
119
+ detectRecordFields: () => detectRecordFields,
116
120
  emailDefinition: () => emailDefinition,
117
121
  fileEditErrors: () => fileEditErrors,
118
122
  fileFormatErrors: () => fileFormatErrors,
@@ -130,6 +134,7 @@ __export(exports_src, {
130
134
  getNodeConfigStatus: () => getNodeConfigStatus,
131
135
  getNodeValidationStatus: () => getNodeValidationStatus,
132
136
  getStorageProvider: () => getStorageProvider,
137
+ groupKeyName: () => groupKeyName,
133
138
  inferSchemaFromSample: () => inferSchemaFromSample,
134
139
  isEditorPrivateKey: () => isEditorPrivateKey,
135
140
  isEventCapableConnector: () => isEventCapableConnector,
@@ -138,6 +143,7 @@ __export(exports_src, {
138
143
  mappingTransformFunctionName: () => mappingTransformFunctionName,
139
144
  migrateDelayConfig: () => migrateDelayConfig,
140
145
  queryDefinition: () => queryDefinition,
146
+ resolveRecordSchema: () => resolveRecordSchema,
141
147
  selectsWithoutValues: () => selectsWithoutValues,
142
148
  storageConfigKeys: () => storageConfigKeys,
143
149
  storageOperations: () => storageOperations,
@@ -202,6 +208,7 @@ var ActionType;
202
208
  ActionType["SpeechToText"] = "speech_to_text";
203
209
  ActionType["TextToSpeech"] = "text_to_speech";
204
210
  ActionType["Transform"] = "transform";
211
+ ActionType["Aggregate"] = "aggregate";
205
212
  ActionType["FileOperation"] = "file_operation";
206
213
  ActionType["FileAction"] = "file_action";
207
214
  ActionType["CsvAction"] = "csv_action";
@@ -1530,7 +1537,7 @@ var queryDefinition = {
1530
1537
  key: "visualConfig.limit",
1531
1538
  label: "Limit",
1532
1539
  type: "number" /* Number */,
1533
- description: "Maximum number of rows to return",
1540
+ description: "Rows the PREVIEW shows. A run never reads it — set visualConfig.maxRows to cap a run.",
1534
1541
  defaultValue: 10,
1535
1542
  validation: { required: false, min: 1, max: 1e4 },
1536
1543
  conditional: [
@@ -1542,6 +1549,22 @@ var queryDefinition = {
1542
1549
  }
1543
1550
  ]
1544
1551
  },
1552
+ {
1553
+ key: "visualConfig.maxRows",
1554
+ label: "Max rows (run)",
1555
+ type: "number" /* Number */,
1556
+ description: "The most rows a run reads (up to 10,000). Unset: 10,000. The Limit above is preview-only.",
1557
+ validation: { required: false, min: 1, max: 1e4 },
1558
+ advanced: true,
1559
+ conditional: [
1560
+ {
1561
+ field: "mode",
1562
+ operator: "==",
1563
+ value: "visual" /* Visual */,
1564
+ action: "show"
1565
+ }
1566
+ ]
1567
+ },
1545
1568
  {
1546
1569
  key: "sqlConfig.sql",
1547
1570
  label: "SQL Query",
@@ -1588,7 +1611,7 @@ var queryDefinition = {
1588
1611
  key: "maxRows",
1589
1612
  label: "Max Rows",
1590
1613
  type: "number" /* Number */,
1591
- description: "Maximum number of rows to return",
1614
+ description: "Legacy step-level field — NOT read at run. Cap a visual Query with visualConfig.maxRows; in SQL mode write a LIMIT.",
1592
1615
  defaultValue: 1000,
1593
1616
  validation: { required: false, min: 1, max: 1e4 },
1594
1617
  advanced: true
@@ -1744,6 +1767,46 @@ var transformDefinition = {
1744
1767
  testable: true
1745
1768
  };
1746
1769
 
1770
+ // src/catalog/general/aggregate/index.ts
1771
+ var aggregateDefinition = {
1772
+ id: "aggregate",
1773
+ name: "Aggregate",
1774
+ description: "Total, count or average the upstream records into one row — or one row per group — of computed values " + "(sum, count, avg, min, max, count_distinct, first, last). Put it after the step that produces the rows and " + "before the step that writes the result, for example a TOTAL row under the rows a Transform just wrote.",
1775
+ category: "transform" /* Transform */,
1776
+ type: "aggregate" /* Aggregate */,
1777
+ tags: ["aggregate", "sum", "total", "count", "average", "min", "max", "group by", "rollup", "built-in"],
1778
+ status: "stable" /* Stable */,
1779
+ icon: "Sigma",
1780
+ color: "#0043ce",
1781
+ inputSchema: { fields: [] },
1782
+ outputSchema: {
1783
+ type: "object",
1784
+ properties: [
1785
+ {
1786
+ key: "data",
1787
+ type: "array" /* Array */,
1788
+ description: "One row per group (a single row when there is no group-by): the group keys, then each metric under its `as` key"
1789
+ },
1790
+ {
1791
+ key: "_meta",
1792
+ type: "object" /* Object */,
1793
+ description: "rowsIn, groups, and ignored — values skipped for a numeric metric because they were not numbers"
1794
+ }
1795
+ ],
1796
+ sampleOutput: {
1797
+ data: [{ TotalAmount: 12850.4, Invoices: 35 }],
1798
+ _meta: { rowsIn: 35, groups: 1, ignored: 0 }
1799
+ },
1800
+ dynamic: true
1801
+ },
1802
+ version: "1.0.0",
1803
+ createdAt: "2026-10-08T00:00:00Z",
1804
+ updatedAt: "2026-10-08T00:00:00Z",
1805
+ createdBy: "system",
1806
+ isOfficial: true,
1807
+ testable: false
1808
+ };
1809
+
1747
1810
  // src/catalog/storage/csv-action/index.ts
1748
1811
  var csvActionDefinition = {
1749
1812
  id: "csv_action",
@@ -2665,7 +2728,7 @@ var graphqlQueryDefinition = {
2665
2728
  var aiAgentDefinition = {
2666
2729
  id: "ai_agent",
2667
2730
  name: "AI Agent",
2668
- description: "Run a reusable AI agent. Pick a saved agent from /ai/agents; optionally override model, temperature, or instructions per step.",
2731
+ description: "Run a reusable AI agent. Pick a saved agent from /ai/agents; optionally override model, temperature, or instructions per step. " + "When the step before it produces rows (a Query, a Transform), the agent runs ONCE PER ROW with the prompt as the per-row template — " + "reference single fields, never the whole table; put an Aggregate first to call it once. maxRows (default 50) caps the calls.",
2669
2732
  category: "ai" /* AI */,
2670
2733
  type: "ai_agent" /* AIAgent */,
2671
2734
  tags: ["ai", "agent", "chat", "built-in"],
@@ -2685,21 +2748,28 @@ var aiAgentDefinition = {
2685
2748
  key: "input",
2686
2749
  label: "Input",
2687
2750
  type: "string" /* String */,
2688
- description: "Chat agents: a string (template-supported via {{step.field}}). ETL agents: rows array. When blank, falls back to upstream step output."
2751
+ description: "The prompt. Over a row-shaped upstream it is the per-row template: each data reference resolves to the current row. " + "A reference to the whole upstream table is refused. When blank, the upstream output is the input."
2689
2752
  },
2690
2753
  {
2691
2754
  key: "overrides",
2692
2755
  label: "Overrides",
2693
2756
  type: "json" /* JSON */,
2694
2757
  description: "Optional per-step overrides of the agent’s own settings: model, temperature, maxTokens, topP, instructions and instructionsMode."
2758
+ },
2759
+ {
2760
+ key: "maxRows",
2761
+ label: "Max rows",
2762
+ type: "number" /* Number */,
2763
+ description: "Per-row mode: the most model calls this step may make, one per upstream row (default 50, at most 1000)."
2695
2764
  }
2696
2765
  ]
2697
2766
  },
2698
2767
  outputSchema: {
2699
2768
  type: "object",
2700
2769
  properties: [
2701
- { key: "text", type: "string" /* String */, description: "Agent response text (chat agents)" },
2702
- { key: "rows", type: "json" /* JSON */, description: "Output rows (etl agents)" },
2770
+ { key: "text", type: "string" /* String */, description: "Agent response text; in per-row mode, one line per row" },
2771
+ { key: "output", type: "json" /* JSON */, description: "The parsed answer (an object when the agent replies in JSON); in per-row mode, one entry per row" },
2772
+ { key: "rows", type: "json" /* JSON */, description: "Per-row mode: one entry per upstream row" },
2703
2773
  { key: "usage", type: "json" /* JSON */, description: "Token usage + cost: tokensIn, tokensOut, latencyMs, costUsd" },
2704
2774
  { key: "model", type: "string" /* String */, description: "Model that produced the response" },
2705
2775
  { key: "agentVersion", type: "number" /* Number */, description: "Version of the agent definition that ran" }
@@ -2803,7 +2873,8 @@ var actionsRegistry = {
2803
2873
  console_log: consoleLogDefinition,
2804
2874
  delay: delayDefinition,
2805
2875
  query: queryDefinition,
2806
- transform: transformDefinition
2876
+ transform: transformDefinition,
2877
+ aggregate: aggregateDefinition
2807
2878
  },
2808
2879
  ["logic" /* Logic */]: {
2809
2880
  condition: conditionDefinition,
@@ -3781,6 +3852,9 @@ var EmailConfig = {
3781
3852
  }
3782
3853
  }
3783
3854
  };
3855
+ // src/catalog/isOneOf.ts
3856
+ var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3857
+
3784
3858
  // src/catalog/general/transform/config.ts
3785
3859
  var TRANSFORM_MODES = ["visual", "expression"];
3786
3860
  var TRANSFORM_OPERATIONS = [
@@ -3790,7 +3864,6 @@ var TRANSFORM_OPERATIONS = [
3790
3864
  { id: "delete", label: "Delete" }
3791
3865
  ];
3792
3866
  var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
3793
- var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
3794
3867
  var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "sqlite", "google_sheets"]);
3795
3868
  var TransformConfig = {
3796
3869
  defaults: {
@@ -3892,6 +3965,313 @@ var TransformConfig = {
3892
3965
  return `map_${Date.now()}_${randomSuffix(5)}`;
3893
3966
  }
3894
3967
  };
3968
+ // src/catalog/general/aggregate/config.ts
3969
+ var AGGREGATE_FUNCTIONS = [
3970
+ "sum",
3971
+ "count",
3972
+ "avg",
3973
+ "min",
3974
+ "max",
3975
+ "count_distinct",
3976
+ "first",
3977
+ "last"
3978
+ ];
3979
+ var AGGREGATE_FUNCTION_DESCRIPTIONS = {
3980
+ sum: "add up the numeric values of a field",
3981
+ count: "count the records (with a field: the records where it is not blank)",
3982
+ avg: "the mean of the numeric values of a field",
3983
+ min: "the smallest value of a field",
3984
+ max: "the largest value of a field",
3985
+ count_distinct: "how many different values a field has",
3986
+ first: "the field's value on the first record",
3987
+ last: "the field's value on the last record"
3988
+ };
3989
+ var RESERVED_KEYS2 = new Set(["_meta", "_lineage", "_redacted", "_error"]);
3990
+ var KEY_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
3991
+ var NUMBER_VALUED = new Set(["sum", "avg", "count", "count_distinct"]);
3992
+ function groupKeyName(path) {
3993
+ const trimmed = String(path ?? "").trim().replace(/\[\]$/, "");
3994
+ const segments = trimmed.split(".").filter(Boolean);
3995
+ return segments.length > 0 ? segments[segments.length - 1] : trimmed;
3996
+ }
3997
+ function schemaAt(schema, path) {
3998
+ let cur = schema;
3999
+ for (const part of path.replace(/\[\]$/, "").split(".").filter(Boolean)) {
4000
+ if (!cur)
4001
+ return;
4002
+ if (cur.type === "array" && cur.items)
4003
+ cur = cur.items;
4004
+ const next = cur.properties?.[part];
4005
+ if (!next)
4006
+ return;
4007
+ cur = next;
4008
+ }
4009
+ return cur;
4010
+ }
4011
+ function leafOf(node, fallback) {
4012
+ const t = node?.type;
4013
+ if (t === "number" || t === "integer" || t === "string" || t === "boolean" || t === "date")
4014
+ return { type: t };
4015
+ return { type: fallback };
4016
+ }
4017
+ function metricSchema(metric, source) {
4018
+ const what = metric.field ? `${metric.fn} of ${metric.field}` : metric.fn;
4019
+ if (NUMBER_VALUED.has(metric.fn))
4020
+ return { type: "number", description: what };
4021
+ return { ...leafOf(metric.field ? schemaAt(source, metric.field) : undefined, "string"), description: what };
4022
+ }
4023
+ var FUNCTION_LIST = AGGREGATE_FUNCTIONS.join(", ");
4024
+ var AggregateConfig = {
4025
+ schema: {
4026
+ arrayPath: {
4027
+ type: "select",
4028
+ required: false,
4029
+ dynamic: true,
4030
+ description: "Which upstream records to aggregate: a path to an array in the upstream step's output. " + "Leave empty for the records the previous step produced (its data). The values come from that step's data, not from the catalog."
4031
+ },
4032
+ groupBy: {
4033
+ type: "array",
4034
+ required: false,
4035
+ description: 'Record fields (dot-paths) to group by, e.g. ["CustomerRef.name"]. Empty: one row for all records. ' + 'Each key lands on the output row under its last segment ("name").'
4036
+ },
4037
+ metrics: {
4038
+ type: "array",
4039
+ required: true,
4040
+ description: 'What to compute, at least one: [{ "fn", "field", "as", "decimals"? }]. ' + `fn is one of ${FUNCTION_LIST} (` + AGGREGATE_FUNCTIONS.map((fn) => `${fn}: ${AGGREGATE_FUNCTION_DESCRIPTIONS[fn]}`).join("; ") + "). field is the record key it reads (a dot-path; count may omit it to count records). " + "as is the output key. decimals rounds a sum or avg. " + 'Example: [{ "fn": "sum", "field": "Balance", "as": "TotalAmount", "decimals": 2 }, { "fn": "count", "as": "Invoices" }]'
4041
+ },
4042
+ outputAs: {
4043
+ type: "string",
4044
+ required: false,
4045
+ description: "The name later steps use for this result, e.g. totals → {{totals[].TotalAmount}} in a Transform mapping. " + "A message or AI prompt reads it as {{ods://steps/<this step id>/output/data[0]/TotalAmount}} — the mapping form is not resolved there. " + "Defaults to the step name."
4046
+ }
4047
+ },
4048
+ defaults: {
4049
+ arrayPath: "",
4050
+ groupBy: [],
4051
+ metrics: [],
4052
+ outputAs: ""
4053
+ },
4054
+ validate(config) {
4055
+ const errors = [];
4056
+ const metrics = Array.isArray(config.metrics) ? config.metrics : [];
4057
+ if (metrics.length === 0) {
4058
+ errors.push('Add at least one metric — what to compute, e.g. { "fn": "sum", "field": "Balance", "as": "TotalAmount" }.');
4059
+ }
4060
+ const groupKeys = [];
4061
+ const groupBy = config.groupBy;
4062
+ if (groupBy !== undefined && groupBy !== null) {
4063
+ if (!Array.isArray(groupBy)) {
4064
+ errors.push('groupBy must be a list of record fields (dot-paths), e.g. ["CustomerRef.name"].');
4065
+ } else {
4066
+ for (const g of groupBy) {
4067
+ if (typeof g !== "string" || g.trim() === "") {
4068
+ errors.push("groupBy has an empty entry — each one is a record field (dot-path).");
4069
+ continue;
4070
+ }
4071
+ groupKeys.push(groupKeyName(g));
4072
+ }
4073
+ }
4074
+ }
4075
+ const seen = new Set;
4076
+ for (const key of groupKeys) {
4077
+ if (seen.has(key)) {
4078
+ errors.push(`Two group-by fields both land under "${key}" — group by fields whose last segment differs.`);
4079
+ }
4080
+ seen.add(key);
4081
+ }
4082
+ metrics.forEach((m, i) => {
4083
+ const where = m && typeof m === "object" && typeof m.as === "string" && m.as ? `"${m.as}"` : `metric ${i + 1}`;
4084
+ if (!m || typeof m !== "object") {
4085
+ errors.push(`${where}: a metric is an object { "fn", "field", "as" }.`);
4086
+ return;
4087
+ }
4088
+ const fn = m.fn;
4089
+ const fnValid = isOneOf(AGGREGATE_FUNCTIONS, fn);
4090
+ if (fn === undefined || fn === null || fn === "") {
4091
+ errors.push(`${where}: fn is required — one of ${FUNCTION_LIST}.`);
4092
+ } else if (!fnValid) {
4093
+ errors.push(`${where}: fn must be one of: ${FUNCTION_LIST} (got "${String(fn)}").`);
4094
+ }
4095
+ const field = m.field;
4096
+ if (field !== undefined && field !== null && typeof field !== "string") {
4097
+ errors.push(`${where}: field must be a string — a dot-path on the record, e.g. "Balance".`);
4098
+ } else if (fnValid && fn !== "count" && (typeof field !== "string" || field.trim() === "")) {
4099
+ errors.push(`${where}: ${fn} needs a field — the record key it reads, e.g. "Balance".`);
4100
+ }
4101
+ const as = m.as;
4102
+ if (typeof as !== "string" || as === "") {
4103
+ errors.push(`${where}: as is required — the output key the value lands under, e.g. "TotalAmount".`);
4104
+ } else if (RESERVED_KEYS2.has(as)) {
4105
+ errors.push(`"${as}" is reserved — pick another output key.`);
4106
+ } else if (!KEY_PATTERN2.test(as)) {
4107
+ errors.push(`"${as}" is not a valid output key (letters, digits, underscore; must not start with a digit).`);
4108
+ } else if (seen.has(as)) {
4109
+ errors.push(`Duplicate output key "${as}" — every metric and group-by key lands under its own name.`);
4110
+ } else {
4111
+ seen.add(as);
4112
+ }
4113
+ const decimals = m.decimals;
4114
+ if (decimals !== undefined && decimals !== null) {
4115
+ if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 10) {
4116
+ errors.push(`${where}: decimals must be a whole number from 0 to 10.`);
4117
+ }
4118
+ }
4119
+ });
4120
+ const outputAs = config.outputAs;
4121
+ if (outputAs !== undefined && outputAs !== null && outputAs !== "") {
4122
+ if (typeof outputAs !== "string" || !KEY_PATTERN2.test(outputAs)) {
4123
+ errors.push('outputAs must be a single name (letters, digits, underscore), e.g. "totals" — later steps read {{totals[].<key>}}.');
4124
+ }
4125
+ }
4126
+ const arrayPath = config.arrayPath;
4127
+ if (arrayPath !== undefined && arrayPath !== null && typeof arrayPath !== "string") {
4128
+ errors.push("arrayPath must be a string — a path to an array in the upstream output, or empty for its records.");
4129
+ }
4130
+ return { valid: errors.length === 0, errors };
4131
+ },
4132
+ computeOutputSchema(config, source) {
4133
+ const properties = {};
4134
+ for (const g of Array.isArray(config.groupBy) ? config.groupBy : []) {
4135
+ if (typeof g !== "string" || g.trim() === "")
4136
+ continue;
4137
+ properties[groupKeyName(g)] = { ...leafOf(schemaAt(source, g), "string"), description: `group: ${g}` };
4138
+ }
4139
+ for (const m of Array.isArray(config.metrics) ? config.metrics : []) {
4140
+ if (!m || typeof m !== "object" || typeof m.as !== "string" || !m.as || !isOneOf(AGGREGATE_FUNCTIONS, m.fn))
4141
+ continue;
4142
+ properties[m.as] = metricSchema(m, source);
4143
+ }
4144
+ return { type: "object", properties };
4145
+ },
4146
+ merge(userConfig) {
4147
+ return { ...AggregateConfig.defaults, ...userConfig };
4148
+ },
4149
+ getLabel(config) {
4150
+ const metrics = Array.isArray(config.metrics) ? config.metrics : [];
4151
+ const parts = metrics.filter((m) => m && typeof m === "object" && m.fn).map((m) => m.field ? `${m.fn}(${m.field})` : String(m.fn));
4152
+ const by = Array.isArray(config.groupBy) && config.groupBy.length > 0 ? ` by ${config.groupBy.map(groupKeyName).join(", ")}` : "";
4153
+ return parts.length > 0 ? `${parts.join(", ")}${by}` : "Aggregate";
4154
+ }
4155
+ };
4156
+ // src/catalog/logic/for-each/recordSchema.ts
4157
+ function resolveSchemaPath(root, path) {
4158
+ if (!root)
4159
+ return;
4160
+ const parts = path.split(".").filter(Boolean);
4161
+ let cur = root;
4162
+ for (const part of parts) {
4163
+ if (cur?.type === "object" && cur.properties && part in cur.properties) {
4164
+ cur = cur.properties[part];
4165
+ continue;
4166
+ }
4167
+ if (cur?.type === "array" && cur.items?.properties && part in cur.items.properties) {
4168
+ cur = cur.items.properties[part];
4169
+ continue;
4170
+ }
4171
+ return;
4172
+ }
4173
+ if (cur?.type === "array" && cur.items)
4174
+ return cur.items;
4175
+ if (cur?.type === "object" && cur.properties)
4176
+ return cur;
4177
+ return;
4178
+ }
4179
+ function resolveSamplePath(sample, path) {
4180
+ const parts = path.split(".").filter(Boolean);
4181
+ let cur = sample;
4182
+ for (const part of parts) {
4183
+ if (cur === null || cur === undefined)
4184
+ return;
4185
+ if (Array.isArray(cur)) {
4186
+ if (cur.length === 0 || typeof cur[0] !== "object" || cur[0] === null)
4187
+ return;
4188
+ cur = cur[0][part];
4189
+ continue;
4190
+ }
4191
+ if (typeof cur === "object") {
4192
+ cur = cur[part];
4193
+ continue;
4194
+ }
4195
+ return;
4196
+ }
4197
+ return cur;
4198
+ }
4199
+ function sampleRecords(node) {
4200
+ const output = node?.executionData?.output;
4201
+ if (!output)
4202
+ return null;
4203
+ if (Array.isArray(output))
4204
+ return output;
4205
+ const data = output.data;
4206
+ return Array.isArray(data) ? data : null;
4207
+ }
4208
+ var isRootPath = (path) => path === "" || path === "data";
4209
+ function resolveRecordSchema(upstreamNodes, arrayPath) {
4210
+ const node = upstreamNodes?.[0];
4211
+ if (!node)
4212
+ return;
4213
+ const path = (arrayPath || "").trim();
4214
+ const isRoot = isRootPath(path);
4215
+ const schema = node.outputSchema || node.config?._outputSchema;
4216
+ if (schema?.properties) {
4217
+ if (isRoot) {
4218
+ const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
4219
+ if (hasDeepFields)
4220
+ return schema;
4221
+ } else {
4222
+ const resolved = resolveSchemaPath(schema, path);
4223
+ if (resolved)
4224
+ return resolved;
4225
+ }
4226
+ }
4227
+ const records = sampleRecords(node);
4228
+ if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
4229
+ const sample = records[0];
4230
+ if (isRoot)
4231
+ return inferSchemaFromSample(sample);
4232
+ const resolved = resolveSamplePath(sample, path);
4233
+ if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
4234
+ return inferSchemaFromSample(resolved[0]);
4235
+ }
4236
+ if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
4237
+ return inferSchemaFromSample(resolved);
4238
+ }
4239
+ if (!path.includes("."))
4240
+ return inferSchemaFromSample(sample);
4241
+ }
4242
+ return schema?.properties ? schema : undefined;
4243
+ }
4244
+ function detectRecordFields(upstreamNodes, arrayPath) {
4245
+ const set = new Set;
4246
+ const props = resolveRecordSchema(upstreamNodes, arrayPath)?.properties;
4247
+ if (props && typeof props === "object")
4248
+ for (const k of Object.keys(props))
4249
+ set.add(k);
4250
+ const records = sampleRecords(upstreamNodes?.[0]);
4251
+ if (records) {
4252
+ const path = (arrayPath || "").trim();
4253
+ const addKeys = (v) => {
4254
+ if (v && typeof v === "object" && !Array.isArray(v))
4255
+ for (const k of Object.keys(v))
4256
+ set.add(k);
4257
+ };
4258
+ for (const r of records.slice(0, 25)) {
4259
+ if (isRootPath(path)) {
4260
+ addKeys(r);
4261
+ continue;
4262
+ }
4263
+ const at = resolveSamplePath(r, path);
4264
+ if (Array.isArray(at))
4265
+ for (const item of at.slice(0, 25))
4266
+ addKeys(item);
4267
+ else if (at && typeof at === "object")
4268
+ addKeys(at);
4269
+ else if (!path.includes("."))
4270
+ addKeys(r);
4271
+ }
4272
+ }
4273
+ return Array.from(set);
4274
+ }
3895
4275
  // src/catalog/storage/storageCapabilities.ts
3896
4276
  var contentTypeField = {
3897
4277
  key: "contentType",
@@ -3961,7 +4341,18 @@ var STORAGE_PROVIDERS = {
3961
4341
  operations: [
3962
4342
  { value: "list", label: "List files", path: { key: "folderId", label: "Folder", selectMode: "folder" }, fields: [{ key: "query", label: "Search query", type: "string", advanced: true }, { key: "pageSize", label: "Max results", type: "number", defaultValue: 50, advanced: true }] },
3963
4343
  { value: "download", label: "Download file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
3964
- { value: "upload", label: "Upload file", path: { key: "folderId", label: "Destination folder", selectMode: "both" }, content: true, fields: [{ key: "fileName", label: "File name", type: "string", required: true }, { key: "mimeType", label: "Content type", type: "string", advanced: true }] },
4344
+ {
4345
+ value: "upload",
4346
+ label: "Upload file",
4347
+ path: { key: "folderId", label: "Destination folder", selectMode: "both" },
4348
+ content: true,
4349
+ fields: [
4350
+ { key: "fileName", label: "File name", type: "string", required: true },
4351
+ { key: "folderName", label: "Folder path", type: "string", advanced: true, placeholder: "Reports/2026", hint: "Folders under My Drive, separated by /. Missing ones are created on the first run. Ignored when a destination folder is picked." },
4352
+ { key: "ifExists", label: "If the file already exists", type: "select", options: [{ label: "Replace it (Drive keeps the versions)", value: "overwrite" }, { label: "Skip it", value: "skip" }, { label: "Keep both (rename)", value: "rename" }], defaultValue: "overwrite", advanced: true },
4353
+ { key: "mimeType", label: "Content type", type: "string", advanced: true }
4354
+ ]
4355
+ },
3965
4356
  { value: "delete", label: "Delete file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
3966
4357
  { value: "move", label: "Move", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both", required: true } },
3967
4358
  { value: "copy", label: "Copy", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both" }, fields: [{ key: "fileName", label: "New name", type: "string" }] },
@@ -4080,7 +4471,13 @@ var AiAgentConfig = {
4080
4471
  schema: {
4081
4472
  agentId: { type: "string", required: true, description: "Public id of a saved agent (agt_…)" },
4082
4473
  input: { type: "text", required: false, description: "The prompt; may reference upstream data with {{ods://…}} tokens. Empty uses the first upstream output." },
4083
- overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" }
4474
+ overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" },
4475
+ maxRows: {
4476
+ type: "number",
4477
+ required: false,
4478
+ min: 1,
4479
+ description: "Per-row mode: the most model calls this step may make, one per upstream row (default 50, at most 1000). More rows stop the step before any call."
4480
+ }
4084
4481
  },
4085
4482
  validate: (config) => {
4086
4483
  const errors = [];
@@ -4089,6 +4486,9 @@ var AiAgentConfig = {
4089
4486
  if (config.overrides !== undefined && (typeof config.overrides !== "object" || config.overrides === null || Array.isArray(config.overrides))) {
4090
4487
  errors.push("overrides must be an object");
4091
4488
  }
4489
+ if (config.maxRows !== undefined && !(Number.isInteger(config.maxRows) && config.maxRows >= 1 && config.maxRows <= 1000)) {
4490
+ errors.push("maxRows must be a whole number from 1 to 1000");
4491
+ }
4092
4492
  return { valid: errors.length === 0, errors };
4093
4493
  },
4094
4494
  merge: (userConfig) => ({ ...AiAgentConfig.defaults, ...userConfig }),
@@ -5619,6 +6019,7 @@ var CONFIG_CONTRACTS = {
5619
6019
  }
5620
6020
  },
5621
6021
  transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
6022
+ aggregate: { schema: AggregateConfig.schema, validate: asValidator(AggregateConfig.validate) },
5622
6023
  condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
5623
6024
  for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
5624
6025
  file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export { queryDefinition, } from "./catalog/general/query/index";
37
37
  export { isQueryableConnector, } from "./catalog/general/query/config";
38
38
  export { AggregateFunction, type BuildQueryConfig, type ColumnSchema, type ConnectorSchema, JoinType, LogicalOperator, type QueryExecutionResult, type QueryField, type QueryFilter, type QueryFilterGroup, type QueryJoin, type QuerySort, SortDirection, type VisualQueryConfig, } from "./catalog/general/query/types";
39
39
  export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_MODES, TRANSFORM_OPERATIONS, TransformConfig, type TransformConfigType, type TransformOperation, } from "./catalog/general/transform/config";
40
+ export { AGGREGATE_FUNCTIONS, AGGREGATE_FUNCTION_DESCRIPTIONS, AggregateConfig, type AggregateConfigType, type AggregateFn, type AggregateMetric, type AggregateSchemaNode, groupKeyName, } from "./catalog/general/aggregate/config";
41
+ export { detectRecordFields, resolveRecordSchema, type RecordSchemaLike, } from "./catalog/logic/for-each/recordSchema";
40
42
  export { STORAGE_CONNECTOR_IDS, type StorageExtraField, type StoragePathField, getStorageProvider, storageConfigKeys, } from "./catalog/storage/storageCapabilities";
41
43
  export { ManualTriggerConfig } from "./catalog/triggers/manual-trigger/index";
42
44
  export { AiAgentConfig, type AiAgentConfigType, type AiAgentOverrides } from "./catalog/ai/ai-agent/config";