@octaviaflow/flow-rules 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog/actions/console-log/config.d.ts +1 -0
- package/dist/catalog/actions/delay/config.d.ts +1 -0
- package/dist/catalog/actions/http-request/config.d.ts +4 -0
- package/dist/catalog/communication/email-send/config.d.ts +2 -0
- package/dist/catalog/communication/webhook-call/config.d.ts +1 -0
- package/dist/catalog/communication/whatsapp-send/config.d.ts +1 -0
- package/dist/catalog/general/aggregate/config.d.ts +126 -0
- package/dist/catalog/general/aggregate/index.d.ts +11 -0
- package/dist/catalog/general/query/config.d.ts +4 -0
- package/dist/catalog/general/transform/config.d.ts +15 -0
- package/dist/catalog/index.d.ts +2 -1
- package/dist/catalog/isOneOf.d.ts +7 -0
- package/dist/catalog/logic/condition/config.d.ts +1 -0
- package/dist/catalog/logic/for-each/config.d.ts +4 -0
- package/dist/catalog/logic/for-each/recordSchema.d.ts +26 -0
- package/dist/catalog/storage/file-operation/config.d.ts +1 -0
- package/dist/catalog/transforms/format/config.d.ts +1 -0
- package/dist/catalog/triggers/schedule-trigger/config.d.ts +2 -0
- package/dist/catalog/triggers/webhook-trigger/config.d.ts +1 -0
- package/dist/catalog/types.d.ts +1 -0
- package/dist/catalog/utility/debug/config.d.ts +1 -0
- package/dist/index.cjs +445 -22
- package/dist/index.d.ts +4 -2
- package/dist/index.js +445 -22
- package/dist/rules/configContracts.d.ts +25 -2
- package/package.json +1 -1
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,
|
|
@@ -96,6 +99,7 @@ __export(exports_src, {
|
|
|
96
99
|
STORAGE_CONNECTOR_IDS: () => STORAGE_CONNECTOR_IDS,
|
|
97
100
|
SortDirection: () => SortDirection,
|
|
98
101
|
StorageActionConfig: () => StorageActionConfig,
|
|
102
|
+
TRANSFORM_MODES: () => TRANSFORM_MODES,
|
|
99
103
|
TRANSFORM_OPERATIONS: () => TRANSFORM_OPERATIONS,
|
|
100
104
|
TransformConfig: () => TransformConfig,
|
|
101
105
|
USER_FUNCTION_PREFIX: () => USER_FUNCTION_PREFIX,
|
|
@@ -112,6 +116,7 @@ __export(exports_src, {
|
|
|
112
116
|
contractFor: () => contractFor,
|
|
113
117
|
contractVocabularyDrift: () => contractVocabularyDrift,
|
|
114
118
|
declaredKeys: () => declaredKeys,
|
|
119
|
+
detectRecordFields: () => detectRecordFields,
|
|
115
120
|
emailDefinition: () => emailDefinition,
|
|
116
121
|
fileEditErrors: () => fileEditErrors,
|
|
117
122
|
fileFormatErrors: () => fileFormatErrors,
|
|
@@ -129,6 +134,7 @@ __export(exports_src, {
|
|
|
129
134
|
getNodeConfigStatus: () => getNodeConfigStatus,
|
|
130
135
|
getNodeValidationStatus: () => getNodeValidationStatus,
|
|
131
136
|
getStorageProvider: () => getStorageProvider,
|
|
137
|
+
groupKeyName: () => groupKeyName,
|
|
132
138
|
inferSchemaFromSample: () => inferSchemaFromSample,
|
|
133
139
|
isEditorPrivateKey: () => isEditorPrivateKey,
|
|
134
140
|
isEventCapableConnector: () => isEventCapableConnector,
|
|
@@ -137,6 +143,8 @@ __export(exports_src, {
|
|
|
137
143
|
mappingTransformFunctionName: () => mappingTransformFunctionName,
|
|
138
144
|
migrateDelayConfig: () => migrateDelayConfig,
|
|
139
145
|
queryDefinition: () => queryDefinition,
|
|
146
|
+
resolveRecordSchema: () => resolveRecordSchema,
|
|
147
|
+
selectsWithoutValues: () => selectsWithoutValues,
|
|
140
148
|
storageConfigKeys: () => storageConfigKeys,
|
|
141
149
|
storageOperations: () => storageOperations,
|
|
142
150
|
summarizeConnectorTriggerEvents: () => summarizeConnectorTriggerEvents,
|
|
@@ -200,6 +208,7 @@ var ActionType;
|
|
|
200
208
|
ActionType["SpeechToText"] = "speech_to_text";
|
|
201
209
|
ActionType["TextToSpeech"] = "text_to_speech";
|
|
202
210
|
ActionType["Transform"] = "transform";
|
|
211
|
+
ActionType["Aggregate"] = "aggregate";
|
|
203
212
|
ActionType["FileOperation"] = "file_operation";
|
|
204
213
|
ActionType["FileAction"] = "file_action";
|
|
205
214
|
ActionType["CsvAction"] = "csv_action";
|
|
@@ -1411,8 +1420,18 @@ function isQueryableConnector(connector) {
|
|
|
1411
1420
|
}
|
|
1412
1421
|
var BuildQueryContractSchema = {
|
|
1413
1422
|
connectorId: { type: "connection", required: true },
|
|
1414
|
-
mode: {
|
|
1415
|
-
|
|
1423
|
+
mode: {
|
|
1424
|
+
type: "select",
|
|
1425
|
+
required: true,
|
|
1426
|
+
options: ["visual", "sql"],
|
|
1427
|
+
description: "visual: the query is built from visualConfig; sql: the query is the text in sqlConfig.sql"
|
|
1428
|
+
},
|
|
1429
|
+
queryType: {
|
|
1430
|
+
type: "select",
|
|
1431
|
+
required: true,
|
|
1432
|
+
options: ["select", "insert", "update", "delete", "custom"],
|
|
1433
|
+
description: "Only select is a read; the write types exist for the SQL mode of database connectors"
|
|
1434
|
+
},
|
|
1416
1435
|
visualConfig: { type: "object", required: false },
|
|
1417
1436
|
sqlConfig: { type: "object", required: false },
|
|
1418
1437
|
timeout: { type: "number", required: false },
|
|
@@ -1732,6 +1751,46 @@ var transformDefinition = {
|
|
|
1732
1751
|
testable: true
|
|
1733
1752
|
};
|
|
1734
1753
|
|
|
1754
|
+
// src/catalog/general/aggregate/index.ts
|
|
1755
|
+
var aggregateDefinition = {
|
|
1756
|
+
id: "aggregate",
|
|
1757
|
+
name: "Aggregate",
|
|
1758
|
+
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.",
|
|
1759
|
+
category: "transform" /* Transform */,
|
|
1760
|
+
type: "aggregate" /* Aggregate */,
|
|
1761
|
+
tags: ["aggregate", "sum", "total", "count", "average", "min", "max", "group by", "rollup", "built-in"],
|
|
1762
|
+
status: "stable" /* Stable */,
|
|
1763
|
+
icon: "Sigma",
|
|
1764
|
+
color: "#0043ce",
|
|
1765
|
+
inputSchema: { fields: [] },
|
|
1766
|
+
outputSchema: {
|
|
1767
|
+
type: "object",
|
|
1768
|
+
properties: [
|
|
1769
|
+
{
|
|
1770
|
+
key: "data",
|
|
1771
|
+
type: "array" /* Array */,
|
|
1772
|
+
description: "One row per group (a single row when there is no group-by): the group keys, then each metric under its `as` key"
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
key: "_meta",
|
|
1776
|
+
type: "object" /* Object */,
|
|
1777
|
+
description: "rowsIn, groups, and ignored — values skipped for a numeric metric because they were not numbers"
|
|
1778
|
+
}
|
|
1779
|
+
],
|
|
1780
|
+
sampleOutput: {
|
|
1781
|
+
data: [{ TotalAmount: 12850.4, Invoices: 35 }],
|
|
1782
|
+
_meta: { rowsIn: 35, groups: 1, ignored: 0 }
|
|
1783
|
+
},
|
|
1784
|
+
dynamic: true
|
|
1785
|
+
},
|
|
1786
|
+
version: "1.0.0",
|
|
1787
|
+
createdAt: "2026-10-08T00:00:00Z",
|
|
1788
|
+
updatedAt: "2026-10-08T00:00:00Z",
|
|
1789
|
+
createdBy: "system",
|
|
1790
|
+
isOfficial: true,
|
|
1791
|
+
testable: false
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1735
1794
|
// src/catalog/storage/csv-action/index.ts
|
|
1736
1795
|
var csvActionDefinition = {
|
|
1737
1796
|
id: "csv_action",
|
|
@@ -2791,7 +2850,8 @@ var actionsRegistry = {
|
|
|
2791
2850
|
console_log: consoleLogDefinition,
|
|
2792
2851
|
delay: delayDefinition,
|
|
2793
2852
|
query: queryDefinition,
|
|
2794
|
-
transform: transformDefinition
|
|
2853
|
+
transform: transformDefinition,
|
|
2854
|
+
aggregate: aggregateDefinition
|
|
2795
2855
|
},
|
|
2796
2856
|
["logic" /* Logic */]: {
|
|
2797
2857
|
condition: conditionDefinition,
|
|
@@ -3312,7 +3372,7 @@ var ConditionConfig = {
|
|
|
3312
3372
|
condition: ""
|
|
3313
3373
|
},
|
|
3314
3374
|
schema: {
|
|
3315
|
-
mode: { type: "select", required: true },
|
|
3375
|
+
mode: { type: "select", required: true, options: ["visual", "expression"] },
|
|
3316
3376
|
filters: { type: "array", required: false },
|
|
3317
3377
|
condition: { type: "expression", required: false }
|
|
3318
3378
|
},
|
|
@@ -3382,8 +3442,18 @@ var ForEachConfig = {
|
|
|
3382
3442
|
collect: "array"
|
|
3383
3443
|
},
|
|
3384
3444
|
schema: {
|
|
3385
|
-
arrayPath: {
|
|
3386
|
-
|
|
3445
|
+
arrayPath: {
|
|
3446
|
+
type: "select",
|
|
3447
|
+
required: false,
|
|
3448
|
+
dynamic: true,
|
|
3449
|
+
description: "A path to an array in the upstream step's output (the Query's object name, or a field holding a list) — the values come from that step's data, not from the catalog"
|
|
3450
|
+
},
|
|
3451
|
+
iterationMode: {
|
|
3452
|
+
type: "select",
|
|
3453
|
+
required: false,
|
|
3454
|
+
options: ["records", "fields", "range", "nested", "batch", "cursor", "while", "array"],
|
|
3455
|
+
description: "What one iteration is: records (one per record, the default; array is its legacy alias), fields, range, nested, batch, cursor or while"
|
|
3456
|
+
},
|
|
3387
3457
|
itemVariable: { type: "string", required: true },
|
|
3388
3458
|
indexVariable: { type: "string", required: false },
|
|
3389
3459
|
batchSize: { type: "number", required: false },
|
|
@@ -3587,7 +3657,7 @@ function fieldToJsonSchema(f) {
|
|
|
3587
3657
|
// src/catalog/utility/debug/config.ts
|
|
3588
3658
|
var DebugConfig = {
|
|
3589
3659
|
schema: {
|
|
3590
|
-
mode: { type: "select", required: true },
|
|
3660
|
+
mode: { type: "select", required: true, options: ["log", "snapshot", "watch", "assert", "profile"] },
|
|
3591
3661
|
label: { type: "string", required: false },
|
|
3592
3662
|
pinned: { type: "boolean", required: false },
|
|
3593
3663
|
condition: { type: "expression", required: false },
|
|
@@ -3702,7 +3772,12 @@ var EmailConfig = {
|
|
|
3702
3772
|
},
|
|
3703
3773
|
schema: {
|
|
3704
3774
|
connection: { type: "connection", required: true },
|
|
3705
|
-
operation: {
|
|
3775
|
+
operation: {
|
|
3776
|
+
type: "select",
|
|
3777
|
+
required: true,
|
|
3778
|
+
options: ["send", "read", "search", "delete", "move", "mark_read"],
|
|
3779
|
+
description: "Only send runs today (DEV-349); the other values are kept so older steps still parse, and validate() rejects them"
|
|
3780
|
+
},
|
|
3706
3781
|
to: { type: "string", required: false },
|
|
3707
3782
|
cc: { type: "string", required: false },
|
|
3708
3783
|
bcc: { type: "string", required: false },
|
|
@@ -3754,13 +3829,18 @@ var EmailConfig = {
|
|
|
3754
3829
|
}
|
|
3755
3830
|
}
|
|
3756
3831
|
};
|
|
3832
|
+
// src/catalog/isOneOf.ts
|
|
3833
|
+
var isOneOf = (set, value) => typeof value === "string" && set.includes(value);
|
|
3834
|
+
|
|
3757
3835
|
// src/catalog/general/transform/config.ts
|
|
3836
|
+
var TRANSFORM_MODES = ["visual", "expression"];
|
|
3758
3837
|
var TRANSFORM_OPERATIONS = [
|
|
3759
3838
|
{ id: "upsert", label: "Upsert" },
|
|
3760
3839
|
{ id: "insert", label: "Insert" },
|
|
3761
3840
|
{ id: "update", label: "Update" },
|
|
3762
3841
|
{ id: "delete", label: "Delete" }
|
|
3763
3842
|
];
|
|
3843
|
+
var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
|
|
3764
3844
|
var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "sqlite", "google_sheets"]);
|
|
3765
3845
|
var TransformConfig = {
|
|
3766
3846
|
defaults: {
|
|
@@ -3784,11 +3864,30 @@ var TransformConfig = {
|
|
|
3784
3864
|
},
|
|
3785
3865
|
schema: {
|
|
3786
3866
|
name: { type: "string", required: false },
|
|
3787
|
-
operation: {
|
|
3867
|
+
operation: {
|
|
3868
|
+
type: "select",
|
|
3869
|
+
required: true,
|
|
3870
|
+
options: TRANSFORM_OPERATION_IDS,
|
|
3871
|
+
description: "What to do with each mapped record at the target: upsert, insert, update or delete"
|
|
3872
|
+
},
|
|
3788
3873
|
connectorId: { type: "connection", required: true },
|
|
3789
|
-
database: {
|
|
3790
|
-
|
|
3791
|
-
|
|
3874
|
+
database: {
|
|
3875
|
+
type: "text",
|
|
3876
|
+
required: false,
|
|
3877
|
+
description: "Database name for MongoDB / PostgreSQL / MySQL / SQLite; the spreadsheet id for Google Sheets"
|
|
3878
|
+
},
|
|
3879
|
+
objectName: {
|
|
3880
|
+
type: "select",
|
|
3881
|
+
required: true,
|
|
3882
|
+
dynamic: true,
|
|
3883
|
+
description: "The table, entity, object or sheet tab to write to — the names come from the connected app (describe_connector_object)"
|
|
3884
|
+
},
|
|
3885
|
+
mode: {
|
|
3886
|
+
type: "select",
|
|
3887
|
+
required: true,
|
|
3888
|
+
options: TRANSFORM_MODES,
|
|
3889
|
+
description: "How the mappings are authored: visual (the Field Mapper — the only mode the editor offers) or expression (legacy JavaScript). This is not the write mode; that is `operation`."
|
|
3890
|
+
},
|
|
3792
3891
|
mappings: { type: "array", required: false },
|
|
3793
3892
|
expression: { type: "code", required: false }
|
|
3794
3893
|
},
|
|
@@ -3802,6 +3901,12 @@ var TransformConfig = {
|
|
|
3802
3901
|
const errors = [];
|
|
3803
3902
|
if (!config.operation) {
|
|
3804
3903
|
errors.push("Operation is required");
|
|
3904
|
+
} else if (!isOneOf(TRANSFORM_OPERATION_IDS, config.operation)) {
|
|
3905
|
+
errors.push(`Operation must be one of: ${TRANSFORM_OPERATION_IDS.join(", ")} (got "${String(config.operation)}")`);
|
|
3906
|
+
}
|
|
3907
|
+
const mode = config.mode;
|
|
3908
|
+
if (mode !== undefined && mode !== null && mode !== "" && !isOneOf(TRANSFORM_MODES, mode)) {
|
|
3909
|
+
errors.push(`Mode must be one of: ${TRANSFORM_MODES.join(", ")} (got "${String(mode)}"). ` + `Mode is how the mappings are authored, not how rows are written — for that, see operation.`);
|
|
3805
3910
|
}
|
|
3806
3911
|
if (!config.connectorId) {
|
|
3807
3912
|
errors.push("Target connector is required");
|
|
@@ -3837,6 +3942,313 @@ var TransformConfig = {
|
|
|
3837
3942
|
return `map_${Date.now()}_${randomSuffix(5)}`;
|
|
3838
3943
|
}
|
|
3839
3944
|
};
|
|
3945
|
+
// src/catalog/general/aggregate/config.ts
|
|
3946
|
+
var AGGREGATE_FUNCTIONS = [
|
|
3947
|
+
"sum",
|
|
3948
|
+
"count",
|
|
3949
|
+
"avg",
|
|
3950
|
+
"min",
|
|
3951
|
+
"max",
|
|
3952
|
+
"count_distinct",
|
|
3953
|
+
"first",
|
|
3954
|
+
"last"
|
|
3955
|
+
];
|
|
3956
|
+
var AGGREGATE_FUNCTION_DESCRIPTIONS = {
|
|
3957
|
+
sum: "add up the numeric values of a field",
|
|
3958
|
+
count: "count the records (with a field: the records where it is not blank)",
|
|
3959
|
+
avg: "the mean of the numeric values of a field",
|
|
3960
|
+
min: "the smallest value of a field",
|
|
3961
|
+
max: "the largest value of a field",
|
|
3962
|
+
count_distinct: "how many different values a field has",
|
|
3963
|
+
first: "the field's value on the first record",
|
|
3964
|
+
last: "the field's value on the last record"
|
|
3965
|
+
};
|
|
3966
|
+
var RESERVED_KEYS2 = new Set(["_meta", "_lineage", "_redacted", "_error"]);
|
|
3967
|
+
var KEY_PATTERN2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3968
|
+
var NUMBER_VALUED = new Set(["sum", "avg", "count", "count_distinct"]);
|
|
3969
|
+
function groupKeyName(path) {
|
|
3970
|
+
const trimmed = String(path ?? "").trim().replace(/\[\]$/, "");
|
|
3971
|
+
const segments = trimmed.split(".").filter(Boolean);
|
|
3972
|
+
return segments.length > 0 ? segments[segments.length - 1] : trimmed;
|
|
3973
|
+
}
|
|
3974
|
+
function schemaAt(schema, path) {
|
|
3975
|
+
let cur = schema;
|
|
3976
|
+
for (const part of path.replace(/\[\]$/, "").split(".").filter(Boolean)) {
|
|
3977
|
+
if (!cur)
|
|
3978
|
+
return;
|
|
3979
|
+
if (cur.type === "array" && cur.items)
|
|
3980
|
+
cur = cur.items;
|
|
3981
|
+
const next = cur.properties?.[part];
|
|
3982
|
+
if (!next)
|
|
3983
|
+
return;
|
|
3984
|
+
cur = next;
|
|
3985
|
+
}
|
|
3986
|
+
return cur;
|
|
3987
|
+
}
|
|
3988
|
+
function leafOf(node, fallback) {
|
|
3989
|
+
const t = node?.type;
|
|
3990
|
+
if (t === "number" || t === "integer" || t === "string" || t === "boolean" || t === "date")
|
|
3991
|
+
return { type: t };
|
|
3992
|
+
return { type: fallback };
|
|
3993
|
+
}
|
|
3994
|
+
function metricSchema(metric, source) {
|
|
3995
|
+
const what = metric.field ? `${metric.fn} of ${metric.field}` : metric.fn;
|
|
3996
|
+
if (NUMBER_VALUED.has(metric.fn))
|
|
3997
|
+
return { type: "number", description: what };
|
|
3998
|
+
return { ...leafOf(metric.field ? schemaAt(source, metric.field) : undefined, "string"), description: what };
|
|
3999
|
+
}
|
|
4000
|
+
var FUNCTION_LIST = AGGREGATE_FUNCTIONS.join(", ");
|
|
4001
|
+
var AggregateConfig = {
|
|
4002
|
+
schema: {
|
|
4003
|
+
arrayPath: {
|
|
4004
|
+
type: "select",
|
|
4005
|
+
required: false,
|
|
4006
|
+
dynamic: true,
|
|
4007
|
+
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."
|
|
4008
|
+
},
|
|
4009
|
+
groupBy: {
|
|
4010
|
+
type: "array",
|
|
4011
|
+
required: false,
|
|
4012
|
+
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").'
|
|
4013
|
+
},
|
|
4014
|
+
metrics: {
|
|
4015
|
+
type: "array",
|
|
4016
|
+
required: true,
|
|
4017
|
+
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" }]'
|
|
4018
|
+
},
|
|
4019
|
+
outputAs: {
|
|
4020
|
+
type: "string",
|
|
4021
|
+
required: false,
|
|
4022
|
+
description: "The name later steps use for this result, e.g. totals → {{totals[].TotalAmount}}. Defaults to the step name."
|
|
4023
|
+
}
|
|
4024
|
+
},
|
|
4025
|
+
defaults: {
|
|
4026
|
+
arrayPath: "",
|
|
4027
|
+
groupBy: [],
|
|
4028
|
+
metrics: [],
|
|
4029
|
+
outputAs: ""
|
|
4030
|
+
},
|
|
4031
|
+
validate(config) {
|
|
4032
|
+
const errors = [];
|
|
4033
|
+
const metrics = Array.isArray(config.metrics) ? config.metrics : [];
|
|
4034
|
+
if (metrics.length === 0) {
|
|
4035
|
+
errors.push('Add at least one metric — what to compute, e.g. { "fn": "sum", "field": "Balance", "as": "TotalAmount" }.');
|
|
4036
|
+
}
|
|
4037
|
+
const groupKeys = [];
|
|
4038
|
+
const groupBy = config.groupBy;
|
|
4039
|
+
if (groupBy !== undefined && groupBy !== null) {
|
|
4040
|
+
if (!Array.isArray(groupBy)) {
|
|
4041
|
+
errors.push('groupBy must be a list of record fields (dot-paths), e.g. ["CustomerRef.name"].');
|
|
4042
|
+
} else {
|
|
4043
|
+
for (const g of groupBy) {
|
|
4044
|
+
if (typeof g !== "string" || g.trim() === "") {
|
|
4045
|
+
errors.push("groupBy has an empty entry — each one is a record field (dot-path).");
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
groupKeys.push(groupKeyName(g));
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
const seen = new Set;
|
|
4053
|
+
for (const key of groupKeys) {
|
|
4054
|
+
if (seen.has(key)) {
|
|
4055
|
+
errors.push(`Two group-by fields both land under "${key}" — group by fields whose last segment differs.`);
|
|
4056
|
+
}
|
|
4057
|
+
seen.add(key);
|
|
4058
|
+
}
|
|
4059
|
+
metrics.forEach((m, i) => {
|
|
4060
|
+
const where = m && typeof m === "object" && typeof m.as === "string" && m.as ? `"${m.as}"` : `metric ${i + 1}`;
|
|
4061
|
+
if (!m || typeof m !== "object") {
|
|
4062
|
+
errors.push(`${where}: a metric is an object { "fn", "field", "as" }.`);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
const fn = m.fn;
|
|
4066
|
+
const fnValid = isOneOf(AGGREGATE_FUNCTIONS, fn);
|
|
4067
|
+
if (fn === undefined || fn === null || fn === "") {
|
|
4068
|
+
errors.push(`${where}: fn is required — one of ${FUNCTION_LIST}.`);
|
|
4069
|
+
} else if (!fnValid) {
|
|
4070
|
+
errors.push(`${where}: fn must be one of: ${FUNCTION_LIST} (got "${String(fn)}").`);
|
|
4071
|
+
}
|
|
4072
|
+
const field = m.field;
|
|
4073
|
+
if (field !== undefined && field !== null && typeof field !== "string") {
|
|
4074
|
+
errors.push(`${where}: field must be a string — a dot-path on the record, e.g. "Balance".`);
|
|
4075
|
+
} else if (fnValid && fn !== "count" && (typeof field !== "string" || field.trim() === "")) {
|
|
4076
|
+
errors.push(`${where}: ${fn} needs a field — the record key it reads, e.g. "Balance".`);
|
|
4077
|
+
}
|
|
4078
|
+
const as = m.as;
|
|
4079
|
+
if (typeof as !== "string" || as === "") {
|
|
4080
|
+
errors.push(`${where}: as is required — the output key the value lands under, e.g. "TotalAmount".`);
|
|
4081
|
+
} else if (RESERVED_KEYS2.has(as)) {
|
|
4082
|
+
errors.push(`"${as}" is reserved — pick another output key.`);
|
|
4083
|
+
} else if (!KEY_PATTERN2.test(as)) {
|
|
4084
|
+
errors.push(`"${as}" is not a valid output key (letters, digits, underscore; must not start with a digit).`);
|
|
4085
|
+
} else if (seen.has(as)) {
|
|
4086
|
+
errors.push(`Duplicate output key "${as}" — every metric and group-by key lands under its own name.`);
|
|
4087
|
+
} else {
|
|
4088
|
+
seen.add(as);
|
|
4089
|
+
}
|
|
4090
|
+
const decimals = m.decimals;
|
|
4091
|
+
if (decimals !== undefined && decimals !== null) {
|
|
4092
|
+
if (typeof decimals !== "number" || !Number.isInteger(decimals) || decimals < 0 || decimals > 10) {
|
|
4093
|
+
errors.push(`${where}: decimals must be a whole number from 0 to 10.`);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
const outputAs = config.outputAs;
|
|
4098
|
+
if (outputAs !== undefined && outputAs !== null && outputAs !== "") {
|
|
4099
|
+
if (typeof outputAs !== "string" || !KEY_PATTERN2.test(outputAs)) {
|
|
4100
|
+
errors.push('outputAs must be a single name (letters, digits, underscore), e.g. "totals" — later steps read {{totals[].<key>}}.');
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
const arrayPath = config.arrayPath;
|
|
4104
|
+
if (arrayPath !== undefined && arrayPath !== null && typeof arrayPath !== "string") {
|
|
4105
|
+
errors.push("arrayPath must be a string — a path to an array in the upstream output, or empty for its records.");
|
|
4106
|
+
}
|
|
4107
|
+
return { valid: errors.length === 0, errors };
|
|
4108
|
+
},
|
|
4109
|
+
computeOutputSchema(config, source) {
|
|
4110
|
+
const properties = {};
|
|
4111
|
+
for (const g of Array.isArray(config.groupBy) ? config.groupBy : []) {
|
|
4112
|
+
if (typeof g !== "string" || g.trim() === "")
|
|
4113
|
+
continue;
|
|
4114
|
+
properties[groupKeyName(g)] = { ...leafOf(schemaAt(source, g), "string"), description: `group: ${g}` };
|
|
4115
|
+
}
|
|
4116
|
+
for (const m of Array.isArray(config.metrics) ? config.metrics : []) {
|
|
4117
|
+
if (!m || typeof m !== "object" || typeof m.as !== "string" || !m.as || !isOneOf(AGGREGATE_FUNCTIONS, m.fn))
|
|
4118
|
+
continue;
|
|
4119
|
+
properties[m.as] = metricSchema(m, source);
|
|
4120
|
+
}
|
|
4121
|
+
return { type: "object", properties };
|
|
4122
|
+
},
|
|
4123
|
+
merge(userConfig) {
|
|
4124
|
+
return { ...AggregateConfig.defaults, ...userConfig };
|
|
4125
|
+
},
|
|
4126
|
+
getLabel(config) {
|
|
4127
|
+
const metrics = Array.isArray(config.metrics) ? config.metrics : [];
|
|
4128
|
+
const parts = metrics.filter((m) => m && typeof m === "object" && m.fn).map((m) => m.field ? `${m.fn}(${m.field})` : String(m.fn));
|
|
4129
|
+
const by = Array.isArray(config.groupBy) && config.groupBy.length > 0 ? ` by ${config.groupBy.map(groupKeyName).join(", ")}` : "";
|
|
4130
|
+
return parts.length > 0 ? `${parts.join(", ")}${by}` : "Aggregate";
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
// src/catalog/logic/for-each/recordSchema.ts
|
|
4134
|
+
function resolveSchemaPath(root, path) {
|
|
4135
|
+
if (!root)
|
|
4136
|
+
return;
|
|
4137
|
+
const parts = path.split(".").filter(Boolean);
|
|
4138
|
+
let cur = root;
|
|
4139
|
+
for (const part of parts) {
|
|
4140
|
+
if (cur?.type === "object" && cur.properties && part in cur.properties) {
|
|
4141
|
+
cur = cur.properties[part];
|
|
4142
|
+
continue;
|
|
4143
|
+
}
|
|
4144
|
+
if (cur?.type === "array" && cur.items?.properties && part in cur.items.properties) {
|
|
4145
|
+
cur = cur.items.properties[part];
|
|
4146
|
+
continue;
|
|
4147
|
+
}
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
if (cur?.type === "array" && cur.items)
|
|
4151
|
+
return cur.items;
|
|
4152
|
+
if (cur?.type === "object" && cur.properties)
|
|
4153
|
+
return cur;
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
function resolveSamplePath(sample, path) {
|
|
4157
|
+
const parts = path.split(".").filter(Boolean);
|
|
4158
|
+
let cur = sample;
|
|
4159
|
+
for (const part of parts) {
|
|
4160
|
+
if (cur === null || cur === undefined)
|
|
4161
|
+
return;
|
|
4162
|
+
if (Array.isArray(cur)) {
|
|
4163
|
+
if (cur.length === 0 || typeof cur[0] !== "object" || cur[0] === null)
|
|
4164
|
+
return;
|
|
4165
|
+
cur = cur[0][part];
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
if (typeof cur === "object") {
|
|
4169
|
+
cur = cur[part];
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
return cur;
|
|
4175
|
+
}
|
|
4176
|
+
function sampleRecords(node) {
|
|
4177
|
+
const output = node?.executionData?.output;
|
|
4178
|
+
if (!output)
|
|
4179
|
+
return null;
|
|
4180
|
+
if (Array.isArray(output))
|
|
4181
|
+
return output;
|
|
4182
|
+
const data = output.data;
|
|
4183
|
+
return Array.isArray(data) ? data : null;
|
|
4184
|
+
}
|
|
4185
|
+
var isRootPath = (path) => path === "" || path === "data";
|
|
4186
|
+
function resolveRecordSchema(upstreamNodes, arrayPath) {
|
|
4187
|
+
const node = upstreamNodes?.[0];
|
|
4188
|
+
if (!node)
|
|
4189
|
+
return;
|
|
4190
|
+
const path = (arrayPath || "").trim();
|
|
4191
|
+
const isRoot = isRootPath(path);
|
|
4192
|
+
const schema = node.outputSchema || node.config?._outputSchema;
|
|
4193
|
+
if (schema?.properties) {
|
|
4194
|
+
if (isRoot) {
|
|
4195
|
+
const hasDeepFields = Object.values(schema.properties).some((f) => f?.properties || f?.items);
|
|
4196
|
+
if (hasDeepFields)
|
|
4197
|
+
return schema;
|
|
4198
|
+
} else {
|
|
4199
|
+
const resolved = resolveSchemaPath(schema, path);
|
|
4200
|
+
if (resolved)
|
|
4201
|
+
return resolved;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
const records = sampleRecords(node);
|
|
4205
|
+
if (records && records.length > 0 && typeof records[0] === "object" && records[0] !== null) {
|
|
4206
|
+
const sample = records[0];
|
|
4207
|
+
if (isRoot)
|
|
4208
|
+
return inferSchemaFromSample(sample);
|
|
4209
|
+
const resolved = resolveSamplePath(sample, path);
|
|
4210
|
+
if (Array.isArray(resolved) && resolved.length > 0 && typeof resolved[0] === "object" && resolved[0] !== null) {
|
|
4211
|
+
return inferSchemaFromSample(resolved[0]);
|
|
4212
|
+
}
|
|
4213
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
4214
|
+
return inferSchemaFromSample(resolved);
|
|
4215
|
+
}
|
|
4216
|
+
if (!path.includes("."))
|
|
4217
|
+
return inferSchemaFromSample(sample);
|
|
4218
|
+
}
|
|
4219
|
+
return schema?.properties ? schema : undefined;
|
|
4220
|
+
}
|
|
4221
|
+
function detectRecordFields(upstreamNodes, arrayPath) {
|
|
4222
|
+
const set = new Set;
|
|
4223
|
+
const props = resolveRecordSchema(upstreamNodes, arrayPath)?.properties;
|
|
4224
|
+
if (props && typeof props === "object")
|
|
4225
|
+
for (const k of Object.keys(props))
|
|
4226
|
+
set.add(k);
|
|
4227
|
+
const records = sampleRecords(upstreamNodes?.[0]);
|
|
4228
|
+
if (records) {
|
|
4229
|
+
const path = (arrayPath || "").trim();
|
|
4230
|
+
const addKeys = (v) => {
|
|
4231
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
4232
|
+
for (const k of Object.keys(v))
|
|
4233
|
+
set.add(k);
|
|
4234
|
+
};
|
|
4235
|
+
for (const r of records.slice(0, 25)) {
|
|
4236
|
+
if (isRootPath(path)) {
|
|
4237
|
+
addKeys(r);
|
|
4238
|
+
continue;
|
|
4239
|
+
}
|
|
4240
|
+
const at = resolveSamplePath(r, path);
|
|
4241
|
+
if (Array.isArray(at))
|
|
4242
|
+
for (const item of at.slice(0, 25))
|
|
4243
|
+
addKeys(item);
|
|
4244
|
+
else if (at && typeof at === "object")
|
|
4245
|
+
addKeys(at);
|
|
4246
|
+
else if (!path.includes("."))
|
|
4247
|
+
addKeys(r);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return Array.from(set);
|
|
4251
|
+
}
|
|
3840
4252
|
// src/catalog/storage/storageCapabilities.ts
|
|
3841
4253
|
var contentTypeField = {
|
|
3842
4254
|
key: "contentType",
|
|
@@ -5005,6 +5417,7 @@ var ConsoleLogConfig = {
|
|
|
5005
5417
|
level: {
|
|
5006
5418
|
type: "select",
|
|
5007
5419
|
required: false,
|
|
5420
|
+
options: ["info", "warning", "error", "debug"],
|
|
5008
5421
|
description: "Log level for the message"
|
|
5009
5422
|
},
|
|
5010
5423
|
data: {
|
|
@@ -5055,6 +5468,7 @@ var DelayConfig = {
|
|
|
5055
5468
|
unit: {
|
|
5056
5469
|
type: "select",
|
|
5057
5470
|
required: false,
|
|
5471
|
+
options: ["milliseconds", "seconds", "minutes", "hours"],
|
|
5058
5472
|
description: "Time unit for the duration"
|
|
5059
5473
|
}
|
|
5060
5474
|
},
|
|
@@ -5119,10 +5533,10 @@ var HTTPRequestConfig = {
|
|
|
5119
5533
|
expectedContentTypes: []
|
|
5120
5534
|
},
|
|
5121
5535
|
schema: {
|
|
5122
|
-
apiType: { type: "select", required: false },
|
|
5536
|
+
apiType: { type: "select", required: false, options: ["rest", "graphql"] },
|
|
5123
5537
|
baseUrl: { type: "string", required: false },
|
|
5124
5538
|
queryParams: { type: "array", required: false },
|
|
5125
|
-
bodyType: { type: "select", required: false },
|
|
5539
|
+
bodyType: { type: "select", required: false, options: ["none", "json", "raw", "form-data", "xml"] },
|
|
5126
5540
|
formData: { type: "array", required: false },
|
|
5127
5541
|
graphqlQuery: { type: "code", required: false },
|
|
5128
5542
|
graphqlVariables: { type: "json", required: false },
|
|
@@ -5132,11 +5546,12 @@ var HTTPRequestConfig = {
|
|
|
5132
5546
|
retry: { type: "object", required: false },
|
|
5133
5547
|
pagination: { type: "object", required: false },
|
|
5134
5548
|
extract: { type: "object", required: false },
|
|
5135
|
-
responseType: { type: "select", required: false },
|
|
5549
|
+
responseType: { type: "select", required: false, options: ["json", "text", "binary"] },
|
|
5136
5550
|
expectedContentTypes: { type: "array", required: false },
|
|
5137
5551
|
method: {
|
|
5138
5552
|
type: "select",
|
|
5139
5553
|
required: true,
|
|
5554
|
+
options: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
5140
5555
|
description: "HTTP method to use"
|
|
5141
5556
|
},
|
|
5142
5557
|
url: {
|
|
@@ -5326,7 +5741,7 @@ var WebhookCallConfig = {
|
|
|
5326
5741
|
},
|
|
5327
5742
|
schema: {
|
|
5328
5743
|
url: { type: "url", required: true },
|
|
5329
|
-
method: { type: "select", required: true },
|
|
5744
|
+
method: { type: "select", required: true, options: ["POST", "PUT", "PATCH", "GET"] },
|
|
5330
5745
|
payload: { type: "json", required: false },
|
|
5331
5746
|
headers: { type: "json", required: false }
|
|
5332
5747
|
},
|
|
@@ -5351,7 +5766,7 @@ var WhatsappSendConfig = {
|
|
|
5351
5766
|
schema: {
|
|
5352
5767
|
connection: { type: "connection", required: true },
|
|
5353
5768
|
to: { type: "string", required: true },
|
|
5354
|
-
mode: { type: "select", required: true },
|
|
5769
|
+
mode: { type: "select", required: true, options: ["template", "text"] },
|
|
5355
5770
|
templateName: { type: "string", required: false },
|
|
5356
5771
|
templateLanguage: { type: "string", required: false },
|
|
5357
5772
|
templateVariables: { type: "array", required: false },
|
|
@@ -5398,7 +5813,7 @@ var FileOperationConfig = {
|
|
|
5398
5813
|
},
|
|
5399
5814
|
schema: {
|
|
5400
5815
|
connection: { type: "connection", required: true },
|
|
5401
|
-
operation: { type: "select", required: true },
|
|
5816
|
+
operation: { type: "select", required: true, options: ["upload", "download", "delete", "list", "copy", "move", "exists"] },
|
|
5402
5817
|
sourcePath: { type: "string", required: false },
|
|
5403
5818
|
destinationPath: { type: "string", required: true },
|
|
5404
5819
|
fileContent: { type: "string", required: false },
|
|
@@ -5432,7 +5847,7 @@ var FormatConfig = {
|
|
|
5432
5847
|
defaults: { input: "", format: "json" },
|
|
5433
5848
|
schema: {
|
|
5434
5849
|
input: { type: "expression", required: true },
|
|
5435
|
-
format: { type: "select", required: true }
|
|
5850
|
+
format: { type: "select", required: true, options: ["json", "csv", "xml", "yaml"] }
|
|
5436
5851
|
},
|
|
5437
5852
|
validate: (config) => {
|
|
5438
5853
|
const errors = [];
|
|
@@ -5457,9 +5872,9 @@ var ScheduleTriggerConfig = {
|
|
|
5457
5872
|
timezone: "UTC"
|
|
5458
5873
|
},
|
|
5459
5874
|
schema: {
|
|
5460
|
-
scheduleType: { type: "select", required: true },
|
|
5875
|
+
scheduleType: { type: "select", required: true, options: ["interval", "cron", "specific"] },
|
|
5461
5876
|
interval: { type: "number", required: false, min: 1 },
|
|
5462
|
-
intervalUnit: { type: "select", required: false },
|
|
5877
|
+
intervalUnit: { type: "select", required: false, options: ["minutes", "hours", "days", "weeks"] },
|
|
5463
5878
|
cronExpression: { type: "string", required: false },
|
|
5464
5879
|
timezone: { type: "string", required: false }
|
|
5465
5880
|
},
|
|
@@ -5497,7 +5912,7 @@ var WebhookTriggerConfig = {
|
|
|
5497
5912
|
},
|
|
5498
5913
|
schema: {
|
|
5499
5914
|
webhookPath: { type: "string", required: false },
|
|
5500
|
-
method: { type: "select", required: true },
|
|
5915
|
+
method: { type: "select", required: true, options: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
|
|
5501
5916
|
requireAuth: { type: "boolean", required: false }
|
|
5502
5917
|
},
|
|
5503
5918
|
validate: (config) => {
|
|
@@ -5561,6 +5976,7 @@ var CONFIG_CONTRACTS = {
|
|
|
5561
5976
|
}
|
|
5562
5977
|
},
|
|
5563
5978
|
transform: { schema: TransformConfig.schema, validate: asValidator(TransformConfig.validate) },
|
|
5979
|
+
aggregate: { schema: AggregateConfig.schema, validate: asValidator(AggregateConfig.validate) },
|
|
5564
5980
|
condition: { schema: ConditionConfig.schema, validate: asValidator(ConditionConfig.validate) },
|
|
5565
5981
|
for_each: { schema: ForEachConfig.schema, validate: asValidator(ForEachConfig.validate) },
|
|
5566
5982
|
file_operation: { schema: FileOperationConfig.schema, validate: asValidator(FileOperationConfig.validate) },
|
|
@@ -5631,11 +6047,18 @@ function contractFieldsFor(definition) {
|
|
|
5631
6047
|
label: humanise(key),
|
|
5632
6048
|
type: CONTRACT_TYPE_TO_FIELD_TYPE[field.type] ?? "string" /* String */,
|
|
5633
6049
|
validation: { required: field.required === true },
|
|
6050
|
+
...typeof field.description === "string" && field.description ? { description: field.description } : {},
|
|
5634
6051
|
...field.options ? { options: field.options.map((v) => ({ label: String(v), value: v })) } : {}
|
|
5635
6052
|
});
|
|
5636
6053
|
}
|
|
5637
6054
|
return [...declared, ...derived];
|
|
5638
6055
|
}
|
|
6056
|
+
function selectsWithoutValues(actionId) {
|
|
6057
|
+
const contract = contractFor(actionId);
|
|
6058
|
+
if (!contract)
|
|
6059
|
+
return [];
|
|
6060
|
+
return Object.entries(contract.schema).filter(([, f]) => f.type === "select" && !(f.options && f.options.length > 0) && f.dynamic !== true).map(([key]) => key);
|
|
6061
|
+
}
|
|
5639
6062
|
function contractDrift(definition) {
|
|
5640
6063
|
const contract = contractFor(definition.id);
|
|
5641
6064
|
if (!contract)
|