@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/catalog/ai/ai-agent/config.d.ts +12 -0
- package/dist/catalog/general/aggregate/config.d.ts +126 -0
- package/dist/catalog/general/aggregate/index.d.ts +11 -0
- package/dist/catalog/index.d.ts +2 -1
- package/dist/catalog/isOneOf.d.ts +7 -0
- package/dist/catalog/logic/for-each/recordSchema.d.ts +26 -0
- package/dist/catalog/types.d.ts +1 -0
- package/dist/index.cjs +411 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +411 -10
- package/dist/rules/configContracts.d.ts +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ var ActionType;
|
|
|
45
45
|
ActionType["SpeechToText"] = "speech_to_text";
|
|
46
46
|
ActionType["TextToSpeech"] = "text_to_speech";
|
|
47
47
|
ActionType["Transform"] = "transform";
|
|
48
|
+
ActionType["Aggregate"] = "aggregate";
|
|
48
49
|
ActionType["FileOperation"] = "file_operation";
|
|
49
50
|
ActionType["FileAction"] = "file_action";
|
|
50
51
|
ActionType["CsvAction"] = "csv_action";
|
|
@@ -1373,7 +1374,7 @@ var queryDefinition = {
|
|
|
1373
1374
|
key: "visualConfig.limit",
|
|
1374
1375
|
label: "Limit",
|
|
1375
1376
|
type: "number" /* Number */,
|
|
1376
|
-
description: "
|
|
1377
|
+
description: "Rows the PREVIEW shows. A run never reads it — set visualConfig.maxRows to cap a run.",
|
|
1377
1378
|
defaultValue: 10,
|
|
1378
1379
|
validation: { required: false, min: 1, max: 1e4 },
|
|
1379
1380
|
conditional: [
|
|
@@ -1385,6 +1386,22 @@ var queryDefinition = {
|
|
|
1385
1386
|
}
|
|
1386
1387
|
]
|
|
1387
1388
|
},
|
|
1389
|
+
{
|
|
1390
|
+
key: "visualConfig.maxRows",
|
|
1391
|
+
label: "Max rows (run)",
|
|
1392
|
+
type: "number" /* Number */,
|
|
1393
|
+
description: "The most rows a run reads (up to 10,000). Unset: 10,000. The Limit above is preview-only.",
|
|
1394
|
+
validation: { required: false, min: 1, max: 1e4 },
|
|
1395
|
+
advanced: true,
|
|
1396
|
+
conditional: [
|
|
1397
|
+
{
|
|
1398
|
+
field: "mode",
|
|
1399
|
+
operator: "==",
|
|
1400
|
+
value: "visual" /* Visual */,
|
|
1401
|
+
action: "show"
|
|
1402
|
+
}
|
|
1403
|
+
]
|
|
1404
|
+
},
|
|
1388
1405
|
{
|
|
1389
1406
|
key: "sqlConfig.sql",
|
|
1390
1407
|
label: "SQL Query",
|
|
@@ -1431,7 +1448,7 @@ var queryDefinition = {
|
|
|
1431
1448
|
key: "maxRows",
|
|
1432
1449
|
label: "Max Rows",
|
|
1433
1450
|
type: "number" /* Number */,
|
|
1434
|
-
description: "
|
|
1451
|
+
description: "Legacy step-level field — NOT read at run. Cap a visual Query with visualConfig.maxRows; in SQL mode write a LIMIT.",
|
|
1435
1452
|
defaultValue: 1000,
|
|
1436
1453
|
validation: { required: false, min: 1, max: 1e4 },
|
|
1437
1454
|
advanced: true
|
|
@@ -1587,6 +1604,46 @@ var transformDefinition = {
|
|
|
1587
1604
|
testable: true
|
|
1588
1605
|
};
|
|
1589
1606
|
|
|
1607
|
+
// src/catalog/general/aggregate/index.ts
|
|
1608
|
+
var aggregateDefinition = {
|
|
1609
|
+
id: "aggregate",
|
|
1610
|
+
name: "Aggregate",
|
|
1611
|
+
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.",
|
|
1612
|
+
category: "transform" /* Transform */,
|
|
1613
|
+
type: "aggregate" /* Aggregate */,
|
|
1614
|
+
tags: ["aggregate", "sum", "total", "count", "average", "min", "max", "group by", "rollup", "built-in"],
|
|
1615
|
+
status: "stable" /* Stable */,
|
|
1616
|
+
icon: "Sigma",
|
|
1617
|
+
color: "#0043ce",
|
|
1618
|
+
inputSchema: { fields: [] },
|
|
1619
|
+
outputSchema: {
|
|
1620
|
+
type: "object",
|
|
1621
|
+
properties: [
|
|
1622
|
+
{
|
|
1623
|
+
key: "data",
|
|
1624
|
+
type: "array" /* Array */,
|
|
1625
|
+
description: "One row per group (a single row when there is no group-by): the group keys, then each metric under its `as` key"
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
key: "_meta",
|
|
1629
|
+
type: "object" /* Object */,
|
|
1630
|
+
description: "rowsIn, groups, and ignored — values skipped for a numeric metric because they were not numbers"
|
|
1631
|
+
}
|
|
1632
|
+
],
|
|
1633
|
+
sampleOutput: {
|
|
1634
|
+
data: [{ TotalAmount: 12850.4, Invoices: 35 }],
|
|
1635
|
+
_meta: { rowsIn: 35, groups: 1, ignored: 0 }
|
|
1636
|
+
},
|
|
1637
|
+
dynamic: true
|
|
1638
|
+
},
|
|
1639
|
+
version: "1.0.0",
|
|
1640
|
+
createdAt: "2026-10-08T00:00:00Z",
|
|
1641
|
+
updatedAt: "2026-10-08T00:00:00Z",
|
|
1642
|
+
createdBy: "system",
|
|
1643
|
+
isOfficial: true,
|
|
1644
|
+
testable: false
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1590
1647
|
// src/catalog/storage/csv-action/index.ts
|
|
1591
1648
|
var csvActionDefinition = {
|
|
1592
1649
|
id: "csv_action",
|
|
@@ -2508,7 +2565,7 @@ var graphqlQueryDefinition = {
|
|
|
2508
2565
|
var aiAgentDefinition = {
|
|
2509
2566
|
id: "ai_agent",
|
|
2510
2567
|
name: "AI Agent",
|
|
2511
|
-
description: "Run a reusable AI agent. Pick a saved agent from /ai/agents; optionally override model, temperature, or instructions per step.",
|
|
2568
|
+
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.",
|
|
2512
2569
|
category: "ai" /* AI */,
|
|
2513
2570
|
type: "ai_agent" /* AIAgent */,
|
|
2514
2571
|
tags: ["ai", "agent", "chat", "built-in"],
|
|
@@ -2528,21 +2585,28 @@ var aiAgentDefinition = {
|
|
|
2528
2585
|
key: "input",
|
|
2529
2586
|
label: "Input",
|
|
2530
2587
|
type: "string" /* String */,
|
|
2531
|
-
description: "
|
|
2588
|
+
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."
|
|
2532
2589
|
},
|
|
2533
2590
|
{
|
|
2534
2591
|
key: "overrides",
|
|
2535
2592
|
label: "Overrides",
|
|
2536
2593
|
type: "json" /* JSON */,
|
|
2537
2594
|
description: "Optional per-step overrides of the agent’s own settings: model, temperature, maxTokens, topP, instructions and instructionsMode."
|
|
2595
|
+
},
|
|
2596
|
+
{
|
|
2597
|
+
key: "maxRows",
|
|
2598
|
+
label: "Max rows",
|
|
2599
|
+
type: "number" /* Number */,
|
|
2600
|
+
description: "Per-row mode: the most model calls this step may make, one per upstream row (default 50, at most 1000)."
|
|
2538
2601
|
}
|
|
2539
2602
|
]
|
|
2540
2603
|
},
|
|
2541
2604
|
outputSchema: {
|
|
2542
2605
|
type: "object",
|
|
2543
2606
|
properties: [
|
|
2544
|
-
{ key: "text", type: "string" /* String */, description: "Agent response text
|
|
2545
|
-
{ key: "
|
|
2607
|
+
{ key: "text", type: "string" /* String */, description: "Agent response text; in per-row mode, one line per row" },
|
|
2608
|
+
{ 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" },
|
|
2609
|
+
{ key: "rows", type: "json" /* JSON */, description: "Per-row mode: one entry per upstream row" },
|
|
2546
2610
|
{ key: "usage", type: "json" /* JSON */, description: "Token usage + cost: tokensIn, tokensOut, latencyMs, costUsd" },
|
|
2547
2611
|
{ key: "model", type: "string" /* String */, description: "Model that produced the response" },
|
|
2548
2612
|
{ key: "agentVersion", type: "number" /* Number */, description: "Version of the agent definition that ran" }
|
|
@@ -2646,7 +2710,8 @@ var actionsRegistry = {
|
|
|
2646
2710
|
console_log: consoleLogDefinition,
|
|
2647
2711
|
delay: delayDefinition,
|
|
2648
2712
|
query: queryDefinition,
|
|
2649
|
-
transform: transformDefinition
|
|
2713
|
+
transform: transformDefinition,
|
|
2714
|
+
aggregate: aggregateDefinition
|
|
2650
2715
|
},
|
|
2651
2716
|
["logic" /* Logic */]: {
|
|
2652
2717
|
condition: conditionDefinition,
|
|
@@ -3624,6 +3689,9 @@ var EmailConfig = {
|
|
|
3624
3689
|
}
|
|
3625
3690
|
}
|
|
3626
3691
|
};
|
|
3692
|
+
// src/catalog/isOneOf.ts
|
|
3693
|
+
var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
|
|
3694
|
+
|
|
3627
3695
|
// src/catalog/general/transform/config.ts
|
|
3628
3696
|
var TRANSFORM_MODES = ["visual", "expression"];
|
|
3629
3697
|
var TRANSFORM_OPERATIONS = [
|
|
@@ -3633,7 +3701,6 @@ var TRANSFORM_OPERATIONS = [
|
|
|
3633
3701
|
{ id: "delete", label: "Delete" }
|
|
3634
3702
|
];
|
|
3635
3703
|
var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
|
|
3636
|
-
var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
|
|
3637
3704
|
var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "sqlite", "google_sheets"]);
|
|
3638
3705
|
var TransformConfig = {
|
|
3639
3706
|
defaults: {
|
|
@@ -3735,6 +3802,313 @@ var TransformConfig = {
|
|
|
3735
3802
|
return `map_${Date.now()}_${randomSuffix(5)}`;
|
|
3736
3803
|
}
|
|
3737
3804
|
};
|
|
3805
|
+
// src/catalog/general/aggregate/config.ts
|
|
3806
|
+
var AGGREGATE_FUNCTIONS = [
|
|
3807
|
+
"sum",
|
|
3808
|
+
"count",
|
|
3809
|
+
"avg",
|
|
3810
|
+
"min",
|
|
3811
|
+
"max",
|
|
3812
|
+
"count_distinct",
|
|
3813
|
+
"first",
|
|
3814
|
+
"last"
|
|
3815
|
+
];
|
|
3816
|
+
var AGGREGATE_FUNCTION_DESCRIPTIONS = {
|
|
3817
|
+
sum: "add up the numeric values of a field",
|
|
3818
|
+
count: "count the records (with a field: the records where it is not blank)",
|
|
3819
|
+
avg: "the mean of the numeric values of a field",
|
|
3820
|
+
min: "the smallest value of a field",
|
|
3821
|
+
max: "the largest value of a field",
|
|
3822
|
+
count_distinct: "how many different values a field has",
|
|
3823
|
+
first: "the field's value on the first record",
|
|
3824
|
+
last: "the field's value on the last record"
|
|
3825
|
+
};
|
|
3826
|
+
var RESERVED_KEYS2 = new Set(["_meta", "_lineage", "_redacted", "_error"]);
|
|
3827
|
+
var KEY_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3828
|
+
var NUMBER_VALUED = new Set(["sum", "avg", "count", "count_distinct"]);
|
|
3829
|
+
function groupKeyName(path) {
|
|
3830
|
+
const trimmed = String(path ?? "").trim().replace(/\[\]$/, "");
|
|
3831
|
+
const segments = trimmed.split(".").filter(Boolean);
|
|
3832
|
+
return segments.length > 0 ? segments[segments.length - 1] : trimmed;
|
|
3833
|
+
}
|
|
3834
|
+
function schemaAt(schema, path) {
|
|
3835
|
+
let cur = schema;
|
|
3836
|
+
for (const part of path.replace(/\[\]$/, "").split(".").filter(Boolean)) {
|
|
3837
|
+
if (!cur)
|
|
3838
|
+
return;
|
|
3839
|
+
if (cur.type === "array" && cur.items)
|
|
3840
|
+
cur = cur.items;
|
|
3841
|
+
const next = cur.properties?.[part];
|
|
3842
|
+
if (!next)
|
|
3843
|
+
return;
|
|
3844
|
+
cur = next;
|
|
3845
|
+
}
|
|
3846
|
+
return cur;
|
|
3847
|
+
}
|
|
3848
|
+
function leafOf(node, fallback) {
|
|
3849
|
+
const t = node?.type;
|
|
3850
|
+
if (t === "number" || t === "integer" || t === "string" || t === "boolean" || t === "date")
|
|
3851
|
+
return { type: t };
|
|
3852
|
+
return { type: fallback };
|
|
3853
|
+
}
|
|
3854
|
+
function metricSchema(metric, source) {
|
|
3855
|
+
const what = metric.field ? `${metric.fn} of ${metric.field}` : metric.fn;
|
|
3856
|
+
if (NUMBER_VALUED.has(metric.fn))
|
|
3857
|
+
return { type: "number", description: what };
|
|
3858
|
+
return { ...leafOf(metric.field ? schemaAt(source, metric.field) : undefined, "string"), description: what };
|
|
3859
|
+
}
|
|
3860
|
+
var FUNCTION_LIST = AGGREGATE_FUNCTIONS.join(", ");
|
|
3861
|
+
var AggregateConfig = {
|
|
3862
|
+
schema: {
|
|
3863
|
+
arrayPath: {
|
|
3864
|
+
type: "select",
|
|
3865
|
+
required: false,
|
|
3866
|
+
dynamic: true,
|
|
3867
|
+
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."
|
|
3868
|
+
},
|
|
3869
|
+
groupBy: {
|
|
3870
|
+
type: "array",
|
|
3871
|
+
required: false,
|
|
3872
|
+
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").'
|
|
3873
|
+
},
|
|
3874
|
+
metrics: {
|
|
3875
|
+
type: "array",
|
|
3876
|
+
required: true,
|
|
3877
|
+
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" }]'
|
|
3878
|
+
},
|
|
3879
|
+
outputAs: {
|
|
3880
|
+
type: "string",
|
|
3881
|
+
required: false,
|
|
3882
|
+
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."
|
|
3883
|
+
}
|
|
3884
|
+
},
|
|
3885
|
+
defaults: {
|
|
3886
|
+
arrayPath: "",
|
|
3887
|
+
groupBy: [],
|
|
3888
|
+
metrics: [],
|
|
3889
|
+
outputAs: ""
|
|
3890
|
+
},
|
|
3891
|
+
validate(config) {
|
|
3892
|
+
const errors = [];
|
|
3893
|
+
const metrics = Array.isArray(config.metrics) ? config.metrics : [];
|
|
3894
|
+
if (metrics.length === 0) {
|
|
3895
|
+
errors.push('Add at least one metric — what to compute, e.g. { "fn": "sum", "field": "Balance", "as": "TotalAmount" }.');
|
|
3896
|
+
}
|
|
3897
|
+
const groupKeys = [];
|
|
3898
|
+
const groupBy = config.groupBy;
|
|
3899
|
+
if (groupBy !== undefined && groupBy !== null) {
|
|
3900
|
+
if (!Array.isArray(groupBy)) {
|
|
3901
|
+
errors.push('groupBy must be a list of record fields (dot-paths), e.g. ["CustomerRef.name"].');
|
|
3902
|
+
} else {
|
|
3903
|
+
for (const g of groupBy) {
|
|
3904
|
+
if (typeof g !== "string" || g.trim() === "") {
|
|
3905
|
+
errors.push("groupBy has an empty entry — each one is a record field (dot-path).");
|
|
3906
|
+
continue;
|
|
3907
|
+
}
|
|
3908
|
+
groupKeys.push(groupKeyName(g));
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
const seen = new Set;
|
|
3913
|
+
for (const key of groupKeys) {
|
|
3914
|
+
if (seen.has(key)) {
|
|
3915
|
+
errors.push(`Two group-by fields both land under "${key}" — group by fields whose last segment differs.`);
|
|
3916
|
+
}
|
|
3917
|
+
seen.add(key);
|
|
3918
|
+
}
|
|
3919
|
+
metrics.forEach((m, i) => {
|
|
3920
|
+
const where = m && typeof m === "object" && typeof m.as === "string" && m.as ? `"${m.as}"` : `metric ${i + 1}`;
|
|
3921
|
+
if (!m || typeof m !== "object") {
|
|
3922
|
+
errors.push(`${where}: a metric is an object { "fn", "field", "as" }.`);
|
|
3923
|
+
return;
|
|
3924
|
+
}
|
|
3925
|
+
const fn = m.fn;
|
|
3926
|
+
const fnValid = isOneOf(AGGREGATE_FUNCTIONS, fn);
|
|
3927
|
+
if (fn === undefined || fn === null || fn === "") {
|
|
3928
|
+
errors.push(`${where}: fn is required — one of ${FUNCTION_LIST}.`);
|
|
3929
|
+
} else if (!fnValid) {
|
|
3930
|
+
errors.push(`${where}: fn must be one of: ${FUNCTION_LIST} (got "${String(fn)}").`);
|
|
3931
|
+
}
|
|
3932
|
+
const field = m.field;
|
|
3933
|
+
if (field !== undefined && field !== null && typeof field !== "string") {
|
|
3934
|
+
errors.push(`${where}: field must be a string — a dot-path on the record, e.g. "Balance".`);
|
|
3935
|
+
} else if (fnValid && fn !== "count" && (typeof field !== "string" || field.trim() === "")) {
|
|
3936
|
+
errors.push(`${where}: ${fn} needs a field — the record key it reads, e.g. "Balance".`);
|
|
3937
|
+
}
|
|
3938
|
+
const as = m.as;
|
|
3939
|
+
if (typeof as !== "string" || as === "") {
|
|
3940
|
+
errors.push(`${where}: as is required — the output key the value lands under, e.g. "TotalAmount".`);
|
|
3941
|
+
} else if (RESERVED_KEYS2.has(as)) {
|
|
3942
|
+
errors.push(`"${as}" is reserved — pick another output key.`);
|
|
3943
|
+
} else if (!KEY_PATTERN2.test(as)) {
|
|
3944
|
+
errors.push(`"${as}" is not a valid output key (letters, digits, underscore; must not start with a digit).`);
|
|
3945
|
+
} else if (seen.has(as)) {
|
|
3946
|
+
errors.push(`Duplicate output key "${as}" — every metric and group-by key lands under its own name.`);
|
|
3947
|
+
} else {
|
|
3948
|
+
seen.add(as);
|
|
3949
|
+
}
|
|
3950
|
+
const decimals = m.decimals;
|
|
3951
|
+
if (decimals !== undefined && decimals !== null) {
|
|
3952
|
+
if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 10) {
|
|
3953
|
+
errors.push(`${where}: decimals must be a whole number from 0 to 10.`);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
});
|
|
3957
|
+
const outputAs = config.outputAs;
|
|
3958
|
+
if (outputAs !== undefined && outputAs !== null && outputAs !== "") {
|
|
3959
|
+
if (typeof outputAs !== "string" || !KEY_PATTERN2.test(outputAs)) {
|
|
3960
|
+
errors.push('outputAs must be a single name (letters, digits, underscore), e.g. "totals" — later steps read {{totals[].<key>}}.');
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
const arrayPath = config.arrayPath;
|
|
3964
|
+
if (arrayPath !== undefined && arrayPath !== null && typeof arrayPath !== "string") {
|
|
3965
|
+
errors.push("arrayPath must be a string — a path to an array in the upstream output, or empty for its records.");
|
|
3966
|
+
}
|
|
3967
|
+
return { valid: errors.length === 0, errors };
|
|
3968
|
+
},
|
|
3969
|
+
computeOutputSchema(config, source) {
|
|
3970
|
+
const properties = {};
|
|
3971
|
+
for (const g of Array.isArray(config.groupBy) ? config.groupBy : []) {
|
|
3972
|
+
if (typeof g !== "string" || g.trim() === "")
|
|
3973
|
+
continue;
|
|
3974
|
+
properties[groupKeyName(g)] = { ...leafOf(schemaAt(source, g), "string"), description: `group: ${g}` };
|
|
3975
|
+
}
|
|
3976
|
+
for (const m of Array.isArray(config.metrics) ? config.metrics : []) {
|
|
3977
|
+
if (!m || typeof m !== "object" || typeof m.as !== "string" || !m.as || !isOneOf(AGGREGATE_FUNCTIONS, m.fn))
|
|
3978
|
+
continue;
|
|
3979
|
+
properties[m.as] = metricSchema(m, source);
|
|
3980
|
+
}
|
|
3981
|
+
return { type: "object", properties };
|
|
3982
|
+
},
|
|
3983
|
+
merge(userConfig) {
|
|
3984
|
+
return { ...AggregateConfig.defaults, ...userConfig };
|
|
3985
|
+
},
|
|
3986
|
+
getLabel(config) {
|
|
3987
|
+
const metrics = Array.isArray(config.metrics) ? config.metrics : [];
|
|
3988
|
+
const parts = metrics.filter((m) => m && typeof m === "object" && m.fn).map((m) => m.field ? `${m.fn}(${m.field})` : String(m.fn));
|
|
3989
|
+
const by = Array.isArray(config.groupBy) && config.groupBy.length > 0 ? ` by ${config.groupBy.map(groupKeyName).join(", ")}` : "";
|
|
3990
|
+
return parts.length > 0 ? `${parts.join(", ")}${by}` : "Aggregate";
|
|
3991
|
+
}
|
|
3992
|
+
};
|
|
3993
|
+
// src/catalog/logic/for-each/recordSchema.ts
|
|
3994
|
+
function resolveSchemaPath(root, path) {
|
|
3995
|
+
if (!root)
|
|
3996
|
+
return;
|
|
3997
|
+
const parts = path.split(".").filter(Boolean);
|
|
3998
|
+
let cur = root;
|
|
3999
|
+
for (const part of parts) {
|
|
4000
|
+
if (cur?.type === "object" && cur.properties && part in cur.properties) {
|
|
4001
|
+
cur = cur.properties[part];
|
|
4002
|
+
continue;
|
|
4003
|
+
}
|
|
4004
|
+
if (cur?.type === "array" && cur.items?.properties && part in cur.items.properties) {
|
|
4005
|
+
cur = cur.items.properties[part];
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
if (cur?.type === "array" && cur.items)
|
|
4011
|
+
return cur.items;
|
|
4012
|
+
if (cur?.type === "object" && cur.properties)
|
|
4013
|
+
return cur;
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
function resolveSamplePath(sample, path) {
|
|
4017
|
+
const parts = path.split(".").filter(Boolean);
|
|
4018
|
+
let cur = sample;
|
|
4019
|
+
for (const part of parts) {
|
|
4020
|
+
if (cur === null || cur === undefined)
|
|
4021
|
+
return;
|
|
4022
|
+
if (Array.isArray(cur)) {
|
|
4023
|
+
if (cur.length === 0 || typeof cur[0] !== "object" || cur[0] === null)
|
|
4024
|
+
return;
|
|
4025
|
+
cur = cur[0][part];
|
|
4026
|
+
continue;
|
|
4027
|
+
}
|
|
4028
|
+
if (typeof cur === "object") {
|
|
4029
|
+
cur = cur[part];
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
return cur;
|
|
4035
|
+
}
|
|
4036
|
+
function sampleRecords(node) {
|
|
4037
|
+
const output = node?.executionData?.output;
|
|
4038
|
+
if (!output)
|
|
4039
|
+
return null;
|
|
4040
|
+
if (Array.isArray(output))
|
|
4041
|
+
return output;
|
|
4042
|
+
const data = output.data;
|
|
4043
|
+
return Array.isArray(data) ? data : null;
|
|
4044
|
+
}
|
|
4045
|
+
var isRootPath = (path) => path === "" || path === "data";
|
|
4046
|
+
function resolveRecordSchema(upstreamNodes, arrayPath) {
|
|
4047
|
+
const node = upstreamNodes?.[0];
|
|
4048
|
+
if (!node)
|
|
4049
|
+
return;
|
|
4050
|
+
const path = (arrayPath || "").trim();
|
|
4051
|
+
const isRoot = isRootPath(path);
|
|
4052
|
+
const schema = node.outputSchema || node.config?._outputSchema;
|
|
4053
|
+
if (schema?.properties) {
|
|
4054
|
+
if (isRoot) {
|
|
4055
|
+
const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
|
|
4056
|
+
if (hasDeepFields)
|
|
4057
|
+
return schema;
|
|
4058
|
+
} else {
|
|
4059
|
+
const resolved = resolveSchemaPath(schema, path);
|
|
4060
|
+
if (resolved)
|
|
4061
|
+
return resolved;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
const records = sampleRecords(node);
|
|
4065
|
+
if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
|
|
4066
|
+
const sample = records[0];
|
|
4067
|
+
if (isRoot)
|
|
4068
|
+
return inferSchemaFromSample(sample);
|
|
4069
|
+
const resolved = resolveSamplePath(sample, path);
|
|
4070
|
+
if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
|
|
4071
|
+
return inferSchemaFromSample(resolved[0]);
|
|
4072
|
+
}
|
|
4073
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
4074
|
+
return inferSchemaFromSample(resolved);
|
|
4075
|
+
}
|
|
4076
|
+
if (!path.includes("."))
|
|
4077
|
+
return inferSchemaFromSample(sample);
|
|
4078
|
+
}
|
|
4079
|
+
return schema?.properties ? schema : undefined;
|
|
4080
|
+
}
|
|
4081
|
+
function detectRecordFields(upstreamNodes, arrayPath) {
|
|
4082
|
+
const set = new Set;
|
|
4083
|
+
const props = resolveRecordSchema(upstreamNodes, arrayPath)?.properties;
|
|
4084
|
+
if (props && typeof props === "object")
|
|
4085
|
+
for (const k of Object.keys(props))
|
|
4086
|
+
set.add(k);
|
|
4087
|
+
const records = sampleRecords(upstreamNodes?.[0]);
|
|
4088
|
+
if (records) {
|
|
4089
|
+
const path = (arrayPath || "").trim();
|
|
4090
|
+
const addKeys = (v) => {
|
|
4091
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
4092
|
+
for (const k of Object.keys(v))
|
|
4093
|
+
set.add(k);
|
|
4094
|
+
};
|
|
4095
|
+
for (const r of records.slice(0, 25)) {
|
|
4096
|
+
if (isRootPath(path)) {
|
|
4097
|
+
addKeys(r);
|
|
4098
|
+
continue;
|
|
4099
|
+
}
|
|
4100
|
+
const at = resolveSamplePath(r, path);
|
|
4101
|
+
if (Array.isArray(at))
|
|
4102
|
+
for (const item of at.slice(0, 25))
|
|
4103
|
+
addKeys(item);
|
|
4104
|
+
else if (at && typeof at === "object")
|
|
4105
|
+
addKeys(at);
|
|
4106
|
+
else if (!path.includes("."))
|
|
4107
|
+
addKeys(r);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
return Array.from(set);
|
|
4111
|
+
}
|
|
3738
4112
|
// src/catalog/storage/storageCapabilities.ts
|
|
3739
4113
|
var contentTypeField = {
|
|
3740
4114
|
key: "contentType",
|
|
@@ -3804,7 +4178,18 @@ var STORAGE_PROVIDERS = {
|
|
|
3804
4178
|
operations: [
|
|
3805
4179
|
{ 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 }] },
|
|
3806
4180
|
{ value: "download", label: "Download file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
|
|
3807
|
-
{
|
|
4181
|
+
{
|
|
4182
|
+
value: "upload",
|
|
4183
|
+
label: "Upload file",
|
|
4184
|
+
path: { key: "folderId", label: "Destination folder", selectMode: "both" },
|
|
4185
|
+
content: true,
|
|
4186
|
+
fields: [
|
|
4187
|
+
{ key: "fileName", label: "File name", type: "string", required: true },
|
|
4188
|
+
{ 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." },
|
|
4189
|
+
{ 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 },
|
|
4190
|
+
{ key: "mimeType", label: "Content type", type: "string", advanced: true }
|
|
4191
|
+
]
|
|
4192
|
+
},
|
|
3808
4193
|
{ value: "delete", label: "Delete file", path: { key: "fileId", label: "File", selectMode: "both", required: true } },
|
|
3809
4194
|
{ value: "move", label: "Move", path: { key: "fileId", label: "File", selectMode: "both", required: true }, dest: { key: "destFolderId", label: "Destination folder", selectMode: "both", required: true } },
|
|
3810
4195
|
{ 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" }] },
|
|
@@ -3923,7 +4308,13 @@ var AiAgentConfig = {
|
|
|
3923
4308
|
schema: {
|
|
3924
4309
|
agentId: { type: "string", required: true, description: "Public id of a saved agent (agt_…)" },
|
|
3925
4310
|
input: { type: "text", required: false, description: "The prompt; may reference upstream data with {{ods://…}} tokens. Empty uses the first upstream output." },
|
|
3926
|
-
overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" }
|
|
4311
|
+
overrides: { type: "object", required: false, description: "Per-step overrides: model, temperature, topP, maxTokens, reasoningEffort, verbosity, batchSize, instructions, instructionsMode, expectedSchema" },
|
|
4312
|
+
maxRows: {
|
|
4313
|
+
type: "number",
|
|
4314
|
+
required: false,
|
|
4315
|
+
min: 1,
|
|
4316
|
+
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."
|
|
4317
|
+
}
|
|
3927
4318
|
},
|
|
3928
4319
|
validate: (config) => {
|
|
3929
4320
|
const errors = [];
|
|
@@ -3932,6 +4323,9 @@ var AiAgentConfig = {
|
|
|
3932
4323
|
if (config.overrides !== undefined && (typeof config.overrides !== "object" || config.overrides === null || Array.isArray(config.overrides))) {
|
|
3933
4324
|
errors.push("overrides must be an object");
|
|
3934
4325
|
}
|
|
4326
|
+
if (config.maxRows !== undefined && !(Number.isInteger(config.maxRows) && config.maxRows >= 1 && config.maxRows <= 1000)) {
|
|
4327
|
+
errors.push("maxRows must be a whole number from 1 to 1000");
|
|
4328
|
+
}
|
|
3935
4329
|
return { valid: errors.length === 0, errors };
|
|
3936
4330
|
},
|
|
3937
4331
|
merge: (userConfig) => ({ ...AiAgentConfig.defaults, ...userConfig }),
|
|
@@ -5462,6 +5856,7 @@ var CONFIG_CONTRACTS = {
|
|
|
5462
5856
|
}
|
|
5463
5857
|
},
|
|
5464
5858
|
transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
|
|
5859
|
+
aggregate: { schema: AggregateConfig.schema, validate: asValidator(AggregateConfig.validate) },
|
|
5465
5860
|
condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
|
|
5466
5861
|
for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
|
|
5467
5862
|
file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
|
|
@@ -5813,9 +6208,12 @@ function mappingTransformFunctionName(transform) {
|
|
|
5813
6208
|
return match ? match[1] : null;
|
|
5814
6209
|
}
|
|
5815
6210
|
export {
|
|
6211
|
+
AGGREGATE_FUNCTIONS,
|
|
6212
|
+
AGGREGATE_FUNCTION_DESCRIPTIONS,
|
|
5816
6213
|
ActionCategory,
|
|
5817
6214
|
ActionStatus,
|
|
5818
6215
|
ActionType,
|
|
6216
|
+
AggregateConfig,
|
|
5819
6217
|
AggregateFunction,
|
|
5820
6218
|
AiAgentConfig,
|
|
5821
6219
|
CONDITION_OPERATORS,
|
|
@@ -5887,6 +6285,7 @@ export {
|
|
|
5887
6285
|
contractFor,
|
|
5888
6286
|
contractVocabularyDrift,
|
|
5889
6287
|
declaredKeys,
|
|
6288
|
+
detectRecordFields,
|
|
5890
6289
|
emailDefinition,
|
|
5891
6290
|
fileEditErrors,
|
|
5892
6291
|
fileFormatErrors,
|
|
@@ -5904,6 +6303,7 @@ export {
|
|
|
5904
6303
|
getNodeConfigStatus,
|
|
5905
6304
|
getNodeValidationStatus,
|
|
5906
6305
|
getStorageProvider,
|
|
6306
|
+
groupKeyName,
|
|
5907
6307
|
inferSchemaFromSample,
|
|
5908
6308
|
isEditorPrivateKey,
|
|
5909
6309
|
isEventCapableConnector,
|
|
@@ -5912,6 +6312,7 @@ export {
|
|
|
5912
6312
|
mappingTransformFunctionName,
|
|
5913
6313
|
migrateDelayConfig,
|
|
5914
6314
|
queryDefinition,
|
|
6315
|
+
resolveRecordSchema,
|
|
5915
6316
|
selectsWithoutValues,
|
|
5916
6317
|
storageConfigKeys,
|
|
5917
6318
|
storageOperations,
|
|
@@ -70,10 +70,10 @@ export interface ConfigContract {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Every action in the catalog, keyed by action id — since 0.11.0 all
|
|
73
|
+
* Every action in the catalog, keyed by action id — since 0.11.0 all
|
|
74
74
|
* definitions carry one (the last fifteen: group, ai_agent, idp_extract,
|
|
75
75
|
* connector_trigger, the five file-format actions and the six storage
|
|
76
|
-
* actions). `contractFor` is undefined only for an id the catalog does not
|
|
76
|
+
* actions; 0.13.0 added `aggregate`, the 38th). `contractFor` is undefined only for an id the catalog does not
|
|
77
77
|
* know, and then consumers fall back to `inputSchema` alone. A new action is
|
|
78
78
|
* a `config.ts` plus one line here; the drift test covers it from then on.
|
|
79
79
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octaviaflow/flow-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "The flow action catalog and the rules engine behind Flow Doctor — one definition of what a step accepts and what makes a flow valid, shared by the editor and the server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|