@octaviaflow/flow-rules 0.16.0 → 0.18.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.
@@ -29,7 +29,7 @@ export interface FieldMapping {
29
29
  /**
30
30
  * "Find by" (DEV-480): the mapped value names a record in the target app —
31
31
  * the write uses the id of the `object` record whose `matchField` equals it.
32
- * Root level only; on a line field it applies to each line. Odoo today.
32
+ * Root level only; on a line field it applies to each line. Odoo and QuickBooks (DEV-487).
33
33
  */
34
34
  lookup?: TransformLookup;
35
35
  }
@@ -46,10 +46,26 @@ export interface TransformLookup {
46
46
  * `lineField` become a list of line objects under `into`, in row order.
47
47
  */
48
48
  export interface TransformGroupBy {
49
- /** Source expression of the key, written like a sourceField: `{{orders[].number}}`. */
50
- key: string;
49
+ /** Source expression of the key, written like a sourceField: `{{orders[].number}}`. Optional with `linesFrom`. */
50
+ key?: string;
51
51
  /** Target field holding the list of lines: `line_items` (Shopify), `Line` (QuickBooks), `order_line` (Odoo). */
52
52
  into: string;
53
+ /**
54
+ * A list on each row that supplies its lines (DEV-484): `{{orders[].line_items}}`.
55
+ * Each element makes one line; a line field under the list
56
+ * (`{{orders[].line_items[].quantity}}`) reads the element, any other reads the row.
57
+ */
58
+ linesFrom?: string;
59
+ }
60
+ /**
61
+ * One row per element of a list on each row (DEV-484): an order becomes one
62
+ * row per line item, each carrying the order's fields.
63
+ */
64
+ export interface TransformUnwind {
65
+ /** The list: `{{orders[].line_items}}`. */
66
+ path: string;
67
+ /** Keep a row whose list is empty, as one row with no element. Default false. */
68
+ keepEmpty?: boolean;
53
69
  }
54
70
  export type TransformMode = 'visual' | 'expression';
55
71
  /**
@@ -91,8 +107,10 @@ export interface TransformConfigType {
91
107
  /** Explicit upsert/update match field (legacy externalid/pkey). Empty =
92
108
  * per-connector auto-detection (Mongo _id, SF Id, SQL primary key). */
93
109
  upsertMatchKey?: string;
94
- /** One record per key, with the `lineField` mappings collected into a list (DEV-478). */
110
+ /** One record per key, or per row with lines from a list on it — the `lineField` mappings collected into a list (DEV-478, DEV-484). */
95
111
  groupBy?: TransformGroupBy;
112
+ /** One row per element of a list on each row, before mapping (DEV-484). */
113
+ unwind?: TransformUnwind;
96
114
  /** Ordered column header names — the exact columns + header text written. */
97
115
  sheetHeaders?: string[];
98
116
  /** Create the target tab on first write if it doesn't exist (default true). */
@@ -152,6 +170,11 @@ export declare const TransformConfig: {
152
170
  required: boolean;
153
171
  description: string;
154
172
  };
173
+ unwind: {
174
+ type: string;
175
+ required: boolean;
176
+ description: string;
177
+ };
155
178
  expression: {
156
179
  type: string;
157
180
  required: boolean;
@@ -50,7 +50,7 @@ export interface ForEachConfigType {
50
50
  /** dot-path on each item; items whose key was already seen are skipped. */
51
51
  idempotencyKeyPath?: string;
52
52
  /** how per-item results are collected (default 'array'). */
53
- collect?: 'array' | 'none' | 'groupBy' | 'reduce';
53
+ collect?: 'array' | 'none' | 'groupBy' | 'reduce' | 'rows';
54
54
  /** groupBy — dot-path on each result used as the group key. */
55
55
  groupByPath?: string;
56
56
  /** reduce — built-in reduction over successful results. */
@@ -93,6 +93,12 @@ export declare const ForEachConfig: {
93
93
  type: string;
94
94
  required: boolean;
95
95
  };
96
+ collect: {
97
+ type: string;
98
+ required: boolean;
99
+ options: string[];
100
+ description: string;
101
+ };
96
102
  };
97
103
  validate: (config: Partial<ForEachConfigType>) => {
98
104
  valid: boolean;
@@ -66,7 +66,7 @@ export declare const CsvActionConfig: {
66
66
  readonly type: "select";
67
67
  readonly required: false;
68
68
  readonly options: readonly import("../fileEditContract").FileEditDestination[];
69
- readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites an S3 source";
69
+ readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites the source file";
70
70
  };
71
71
  outputFileName: {
72
72
  readonly type: "string";
@@ -82,7 +82,7 @@ export declare const CsvActionConfig: {
82
82
  s3Key: {
83
83
  readonly type: "string";
84
84
  readonly required: false;
85
- readonly description: "source \"s3\": platform-bucket key of the file to edit";
85
+ readonly description: "source \"s3\": an earlier step's file (its s3Key output) to edit";
86
86
  };
87
87
  url: {
88
88
  readonly type: "url";
@@ -67,7 +67,7 @@ export declare const ExcelActionConfig: {
67
67
  readonly type: "select";
68
68
  readonly required: false;
69
69
  readonly options: readonly import("../fileEditContract").FileEditDestination[];
70
- readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites an S3 source";
70
+ readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites the source file";
71
71
  };
72
72
  outputFileName: {
73
73
  readonly type: "string";
@@ -83,7 +83,7 @@ export declare const ExcelActionConfig: {
83
83
  s3Key: {
84
84
  readonly type: "string";
85
85
  readonly required: false;
86
- readonly description: "source \"s3\": platform-bucket key of the file to edit";
86
+ readonly description: "source \"s3\": an earlier step's file (its s3Key output) to edit";
87
87
  };
88
88
  url: {
89
89
  readonly type: "url";
@@ -90,7 +90,7 @@ export declare const FILE_EDIT_SCHEMA: {
90
90
  readonly type: "select";
91
91
  readonly required: false;
92
92
  readonly options: readonly FileEditDestination[];
93
- readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites an S3 source";
93
+ readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites the source file";
94
94
  };
95
95
  readonly outputFileName: {
96
96
  readonly type: "string";
@@ -106,7 +106,7 @@ export declare const FILE_EDIT_SCHEMA: {
106
106
  readonly s3Key: {
107
107
  readonly type: "string";
108
108
  readonly required: false;
109
- readonly description: "source \"s3\": platform-bucket key of the file to edit";
109
+ readonly description: "source \"s3\": an earlier step's file (its s3Key output) to edit";
110
110
  };
111
111
  readonly url: {
112
112
  readonly type: "url";
@@ -0,0 +1,51 @@
1
+ /** Help for the fields a file step outputs, keyed by field name. */
2
+ export declare const FILE_OUTPUT_HELP: {
3
+ readonly operation: "The operation that ran";
4
+ readonly data: "File contents, for text files such as CSV, JSON or TXT";
5
+ readonly s3Key: "The file itself, for PDF, Excel, images and other non-text files. Pass it to Excel, CSV, IDP or an Upload step.";
6
+ readonly fileName: "File name";
7
+ readonly mimeType: "File type, e.g. text/csv or application/pdf";
8
+ readonly path: "Where the file was read from";
9
+ };
10
+ /** Help for an input that takes text or a file from an earlier step. */
11
+ export declare const FILE_INPUT_HELP: {
12
+ /** Upload content: blank takes the file the step before produced. */
13
+ readonly uploadContent: "Leave blank to upload the file from the step before, or pick a value from an earlier step, or type the content.";
14
+ /** A format action's input (CSV / JSON / XML). */
15
+ readonly formatContent: (format: string) => string;
16
+ /** The Excel action's input. */
17
+ readonly excelContent: "The Excel file from an earlier step (a download or a generated file).";
18
+ };
19
+ /**
20
+ * The output of a Storage `download`, as a JSON Schema. The editor stores it on
21
+ * the step so Data References shows bindable fields before the first run.
22
+ */
23
+ export declare const STORAGE_DOWNLOAD_OUTPUT_SCHEMA: {
24
+ readonly type: "object";
25
+ readonly properties: {
26
+ readonly operation: {
27
+ readonly type: "string";
28
+ readonly description: "The operation that ran";
29
+ };
30
+ readonly data: {
31
+ readonly type: "string";
32
+ readonly description: "File contents, for text files such as CSV, JSON or TXT";
33
+ };
34
+ readonly s3Key: {
35
+ readonly type: "string";
36
+ readonly description: "The file itself, for PDF, Excel, images and other non-text files. Pass it to Excel, CSV, IDP or an Upload step.";
37
+ };
38
+ readonly fileName: {
39
+ readonly type: "string";
40
+ readonly description: "File name";
41
+ };
42
+ readonly mimeType: {
43
+ readonly type: "string";
44
+ readonly description: "File type, e.g. text/csv or application/pdf";
45
+ };
46
+ readonly path: {
47
+ readonly type: "string";
48
+ readonly description: "Where the file was read from";
49
+ };
50
+ };
51
+ };
@@ -66,7 +66,7 @@ export declare const JsonActionConfig: {
66
66
  readonly type: "select";
67
67
  readonly required: false;
68
68
  readonly options: readonly import("../fileEditContract").FileEditDestination[];
69
- readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites an S3 source";
69
+ readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites the source file";
70
70
  };
71
71
  outputFileName: {
72
72
  readonly type: "string";
@@ -82,7 +82,7 @@ export declare const JsonActionConfig: {
82
82
  s3Key: {
83
83
  readonly type: "string";
84
84
  readonly required: false;
85
- readonly description: "source \"s3\": platform-bucket key of the file to edit";
85
+ readonly description: "source \"s3\": an earlier step's file (its s3Key output) to edit";
86
86
  };
87
87
  url: {
88
88
  readonly type: "url";
@@ -64,7 +64,7 @@ export declare const XmlActionConfig: {
64
64
  readonly type: "select";
65
65
  readonly required: false;
66
66
  readonly options: readonly import("../fileEditContract").FileEditDestination[];
67
- readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites an S3 source";
67
+ readonly description: "\"new\" writes an edited copy (default); \"in_place\" overwrites the source file";
68
68
  };
69
69
  outputFileName: {
70
70
  readonly type: "string";
@@ -80,7 +80,7 @@ export declare const XmlActionConfig: {
80
80
  s3Key: {
81
81
  readonly type: "string";
82
82
  readonly required: false;
83
- readonly description: "source \"s3\": platform-bucket key of the file to edit";
83
+ readonly description: "source \"s3\": an earlier step's file (its s3Key output) to edit";
84
84
  };
85
85
  url: {
86
86
  readonly type: "url";
package/dist/index.cjs CHANGED
@@ -67,6 +67,8 @@ __export(exports_src, {
67
67
  FILE_EDIT_SCHEMA: () => FILE_EDIT_SCHEMA,
68
68
  FILE_EDIT_SOURCES: () => FILE_EDIT_SOURCES,
69
69
  FILE_FORMAT_OPERATIONS: () => FILE_FORMAT_OPERATIONS,
70
+ FILE_INPUT_HELP: () => FILE_INPUT_HELP,
71
+ FILE_OUTPUT_HELP: () => FILE_OUTPUT_HELP,
70
72
  FTP_OPERATIONS: () => FTP_OPERATIONS,
71
73
  FieldType: () => FieldType,
72
74
  ForEachConfig: () => ForEachConfig,
@@ -92,6 +94,7 @@ __export(exports_src, {
92
94
  S3ActionConfig: () => S3ActionConfig,
93
95
  S3_OPERATIONS: () => S3_OPERATIONS,
94
96
  STORAGE_CONNECTOR_IDS: () => STORAGE_CONNECTOR_IDS,
97
+ STORAGE_DOWNLOAD_OUTPUT_SCHEMA: () => STORAGE_DOWNLOAD_OUTPUT_SCHEMA,
95
98
  SortDirection: () => SortDirection,
96
99
  StorageActionConfig: () => StorageActionConfig,
97
100
  TRANSFORM_MODES: () => TRANSFORM_MODES,
@@ -1784,6 +1787,32 @@ var aggregateDefinition = {
1784
1787
  testable: false
1785
1788
  };
1786
1789
 
1790
+ // src/catalog/storage/fileHelp.ts
1791
+ var FILE_OUTPUT_HELP = {
1792
+ operation: "The operation that ran",
1793
+ data: "File contents, for text files such as CSV, JSON or TXT",
1794
+ s3Key: "The file itself, for PDF, Excel, images and other non-text files. Pass it to Excel, CSV, IDP or an Upload step.",
1795
+ fileName: "File name",
1796
+ mimeType: "File type, e.g. text/csv or application/pdf",
1797
+ path: "Where the file was read from"
1798
+ };
1799
+ var FILE_INPUT_HELP = {
1800
+ uploadContent: "Leave blank to upload the file from the step before, or pick a value from an earlier step, or type the content.",
1801
+ formatContent: (format) => `The ${format} from an earlier step (a download or a generated file), or paste the ${format} here.`,
1802
+ excelContent: "The Excel file from an earlier step (a download or a generated file)."
1803
+ };
1804
+ var STORAGE_DOWNLOAD_OUTPUT_SCHEMA = {
1805
+ type: "object",
1806
+ properties: {
1807
+ operation: { type: "string", description: FILE_OUTPUT_HELP.operation },
1808
+ data: { type: "string", description: FILE_OUTPUT_HELP.data },
1809
+ s3Key: { type: "string", description: FILE_OUTPUT_HELP.s3Key },
1810
+ fileName: { type: "string", description: FILE_OUTPUT_HELP.fileName },
1811
+ mimeType: { type: "string", description: FILE_OUTPUT_HELP.mimeType },
1812
+ path: { type: "string", description: FILE_OUTPUT_HELP.path }
1813
+ }
1814
+ };
1815
+
1787
1816
  // src/catalog/storage/csv-action/index.ts
1788
1817
  var csvActionDefinition = {
1789
1818
  id: "csv_action",
@@ -1798,7 +1827,7 @@ var csvActionDefinition = {
1798
1827
  inputSchema: {
1799
1828
  fields: [
1800
1829
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (CSV → Records)", value: "parse" }, { label: "Generate (Records → CSV)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1801
- { key: "content", label: "CSV Content", type: "string" /* String */, description: "Paste CSV content or use FX token from upstream step" },
1830
+ { key: "content", label: "CSV Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("CSV") },
1802
1831
  { key: "delimiter", label: "Delimiter", type: "select" /* Select */, options: [{ label: "Comma (,)", value: "," }, { label: "Semicolon (;)", value: ";" }, { label: "Tab", value: "\t" }, { label: "Pipe (|)", value: "|" }], defaultValue: "," },
1803
1832
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing column headers (default: 1)", defaultValue: 1 },
1804
1833
  { key: "encoding", label: "Encoding", type: "select" /* Select */, options: [{ label: "UTF-8", value: "utf-8" }, { label: "Latin-1", value: "latin1" }, { label: "ASCII", value: "ascii" }], defaultValue: "utf-8" },
@@ -1838,7 +1867,7 @@ var jsonActionDefinition = {
1838
1867
  fields: [
1839
1868
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (JSON → Records)", value: "parse" }, { label: "Generate (Records → JSON)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1840
1869
  { key: "format", label: "Format", type: "select" /* Select */, options: [{ label: "JSON (array)", value: "json" }, { label: "JSONL (one object per line)", value: "jsonl" }], defaultValue: "json" },
1841
- { key: "content", label: "JSON Content", type: "string" /* String */, description: "Paste JSON content or use FX token from upstream step" },
1870
+ { key: "content", label: "JSON Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("JSON") },
1842
1871
  { key: "prettyPrint", label: "Pretty Print", type: "boolean" /* Boolean */, description: "Format output with indentation", defaultValue: true },
1843
1872
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.json" }
1844
1873
  ]
@@ -1874,7 +1903,7 @@ var xmlActionDefinition = {
1874
1903
  inputSchema: {
1875
1904
  fields: [
1876
1905
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (XML → Records)", value: "parse" }, { label: "Generate (Records → XML)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1877
- { key: "content", label: "XML Content", type: "string" /* String */, description: "Paste XML or use FX token" },
1906
+ { key: "content", label: "XML Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("XML") },
1878
1907
  { key: "rootElement", label: "Root Element", type: "string" /* String */, description: "Root XML element name (for generate)", defaultValue: "data" },
1879
1908
  { key: "rowElement", label: "Row Element", type: "string" /* String */, description: "Element name per record (for generate)", defaultValue: "row" },
1880
1909
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xml" }
@@ -1911,7 +1940,7 @@ var excelActionDefinition = {
1911
1940
  inputSchema: {
1912
1941
  fields: [
1913
1942
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (Excel → Records)", value: "parse" }, { label: "Generate (Records → Excel)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1914
- { key: "content", label: "Excel File", type: "string" /* String */, description: "S3 key of uploaded Excel file or FX token from upstream" },
1943
+ { key: "content", label: "Excel File", type: "string" /* String */, description: FILE_INPUT_HELP.excelContent },
1915
1944
  { key: "sheetName", label: "Sheet Name", type: "string" /* String */, description: "Name of sheet to read (default: first sheet)" },
1916
1945
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing headers", defaultValue: 1 },
1917
1946
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xlsx" }
@@ -1923,7 +1952,7 @@ var excelActionDefinition = {
1923
1952
  { key: "data", type: "array" /* Array */, description: "Parsed records (parse mode) — loop these with a ForEach. File metadata in generate mode." },
1924
1953
  { key: "columns", type: "json" /* JSON */, description: "Column names" },
1925
1954
  { key: "rowCount", type: "number" /* Number */, description: "Number of records" },
1926
- { key: "s3Key", type: "string" /* String */, description: "S3 key of generated file" }
1955
+ { key: "s3Key", type: "string" /* String */, description: "The generated Excel file. Pass it to an Upload step to save it." }
1927
1956
  ],
1928
1957
  sampleOutput: { data: [{ id: 1, name: "Widget" }], columns: ["id", "name"], rowCount: 1 }
1929
1958
  },
@@ -1953,7 +1982,7 @@ var s3ActionDefinition = {
1953
1982
  { key: "bucket", label: "Bucket Name", type: "string" /* String */, description: "S3 bucket name (defaults to the connector’s bucket)" },
1954
1983
  { key: "key", label: "Object Key (Path)", type: "string" /* String */, description: "S3 object key, e.g. data/reports/monthly.csv" },
1955
1984
  { key: "prefix", label: "Prefix (for List)", type: "string" /* String */, description: "Filter objects by prefix (folder path)" },
1956
- { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
1985
+ { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1957
1986
  { key: "contentType", label: "Content Type", type: "string" /* String */, description: "MIME type (e.g. text/csv, application/json)", defaultValue: "application/octet-stream" },
1958
1987
  { key: "destBucket", label: "Destination Bucket (for Copy)", type: "string" /* String */ },
1959
1988
  { key: "destKey", label: "Destination Key (for Copy)", type: "string" /* String */ },
@@ -1994,7 +2023,7 @@ var ftpActionDefinition = {
1994
2023
  { key: "connector", label: "FTP / SFTP Connection", type: "connection" /* Connection */, description: "Select a configured FTP/SFTP connector from Integrations", validation: { required: true } },
1995
2024
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete File", value: "delete" }, { label: "Move / Rename", value: "move" }], defaultValue: "download", validation: { required: true } },
1996
2025
  { key: "remotePath", label: "Remote Path", type: "string" /* String */, description: "File or folder path on the server (use file explorer to browse)" },
1997
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
2026
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1998
2027
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */, description: "Name for the uploaded file" },
1999
2028
  { key: "destPath", label: "Destination Path (Move)", type: "string" /* String */, description: "New path for moved/renamed file" },
2000
2029
  { key: "ifExists", label: "If File Exists", type: "select" /* Select */, options: [{ label: "Overwrite", value: "overwrite" }, { label: "Skip", value: "skip" }], defaultValue: "overwrite" }
@@ -2035,7 +2064,7 @@ var googleDriveActionDefinition = {
2035
2064
  { key: "fileId", label: "File ID / Path", type: "string" /* String */, description: "Google Drive file ID (use file explorer to browse)" },
2036
2065
  { key: "folderId", label: "Folder ID", type: "string" /* String */, description: "Target folder (root if empty)" },
2037
2066
  { key: "fileName", label: "File Name", type: "string" /* String */ },
2038
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2067
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2039
2068
  { key: "mimeType", label: "MIME Type", type: "string" /* String */, defaultValue: "application/octet-stream" },
2040
2069
  { key: "destFolderId", label: "Destination Folder (Move/Copy)", type: "string" /* String */ },
2041
2070
  { key: "shareEmail", label: "Share With Email", type: "string" /* String */ },
@@ -2077,7 +2106,7 @@ var dropboxActionDefinition = {
2077
2106
  { key: "connector", label: "Dropbox Account", type: "connection" /* Connection */, description: "Select a connected Dropbox account", validation: { required: true } },
2078
2107
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }, { label: "Create Folder", value: "create_folder" }, { label: "Get Shared Link", value: "shared_link" }], defaultValue: "download", validation: { required: true } },
2079
2108
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on Dropbox (use file explorer to browse)" },
2080
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2109
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2081
2110
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
2082
2111
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ },
2083
2112
  { key: "mode", label: "Upload Mode", type: "select" /* Select */, options: [{ label: "Add (fail if exists)", value: "add" }, { label: "Overwrite", value: "overwrite" }], defaultValue: "overwrite" }
@@ -2117,7 +2146,7 @@ var onedriveActionDefinition = {
2117
2146
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }], defaultValue: "download", validation: { required: true } },
2118
2147
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on OneDrive (use file explorer to browse)" },
2119
2148
  { key: "itemId", label: "Item ID", type: "string" /* String */, description: "OneDrive item ID (alternative to path)" },
2120
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
2149
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
2121
2150
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
2122
2151
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ }
2123
2152
  ]
@@ -2175,14 +2204,17 @@ var storageActionDefinition = {
2175
2204
  },
2176
2205
  { key: "path", label: "Path", type: "string" /* String */, description: "File or folder path — use the file browser in the panel." },
2177
2206
  { key: "destPath", label: "Destination path", type: "string" /* String */, description: "Target for copy / move." },
2178
- { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: "Content to upload — bind an FX token from an upstream step." }
2207
+ { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent }
2179
2208
  ]
2180
2209
  },
2181
2210
  outputSchema: {
2182
2211
  type: "object",
2183
2212
  properties: [
2184
2213
  { key: "operation", type: "string" /* String */, description: "The operation that ran" },
2185
- { key: "data", type: "json" /* JSON */, description: "Operation result or downloaded content" },
2214
+ { key: "data", type: "json" /* JSON */, description: `Download: ${FILE_OUTPUT_HELP.data}. Other operations: their result.` },
2215
+ { key: "s3Key", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.s3Key}` },
2216
+ { key: "fileName", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.fileName}` },
2217
+ { key: "mimeType", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.mimeType}` },
2186
2218
  { key: "files", type: "json" /* JSON */, description: "Listing (for the List operation)" },
2187
2219
  { key: "path", type: "string" /* String */, description: "Affected file/folder path" },
2188
2220
  { key: "url", type: "string" /* String */, description: "Signed URL (for the Get signed URL operation)" }
@@ -3340,7 +3372,13 @@ var ForEachConfig = {
3340
3372
  itemVariable: { type: "string", required: true },
3341
3373
  indexVariable: { type: "string", required: false },
3342
3374
  batchSize: { type: "number", required: false },
3343
- continueOnError: { type: "boolean", required: false }
3375
+ continueOnError: { type: "boolean", required: false },
3376
+ collect: {
3377
+ type: "select",
3378
+ required: false,
3379
+ options: ["array", "none", "groupBy", "reduce", "rows"],
3380
+ description: "What the Done port carries besides the counts: array (each iteration's result in `results`, the default), none, groupBy (by groupByPath), " + "reduce (reduceOp over reducePath), or rows — also `data`: every row the body produced (each iteration's Query/Transform rows, else its result), " + "so ONE step on the Done port maps and writes them all instead of a write per iteration."
3381
+ }
3344
3382
  },
3345
3383
  validate: (config) => {
3346
3384
  const errors = [];
@@ -3725,6 +3763,34 @@ var TRANSFORM_OPERATIONS = [
3725
3763
  ];
3726
3764
  var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
3727
3765
  var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "google_sheets"]);
3766
+ function groupByErrors(config) {
3767
+ const groupBy = config.groupBy;
3768
+ if (!groupBy || !(groupBy.key || groupBy.into || groupBy.linesFrom))
3769
+ return [];
3770
+ const errors = [];
3771
+ if (!groupBy.key && !groupBy.linesFrom) {
3772
+ errors.push("One record per key: choose the key field (groupBy.key)");
3773
+ }
3774
+ if (!groupBy.into)
3775
+ errors.push("One record per key: name the field that holds the lines (groupBy.into)");
3776
+ if ((groupBy.key || groupBy.linesFrom) && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
3777
+ errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
3778
+ }
3779
+ return errors;
3780
+ }
3781
+ function unwindErrors(config) {
3782
+ const unwind = config.unwind;
3783
+ if (!unwind)
3784
+ return [];
3785
+ if (!unwind.path?.trim()) {
3786
+ return unwind.keepEmpty !== undefined ? ["One row per item: choose the list (unwind.path)"] : [];
3787
+ }
3788
+ const linesFrom = config.groupBy?.linesFrom;
3789
+ if (linesFrom && linesFrom.trim() === unwind.path.trim()) {
3790
+ return ["One row per item and Lines come from name the same list — use one of them (unwind.path, groupBy.linesFrom)"];
3791
+ }
3792
+ return [];
3793
+ }
3728
3794
  var TransformConfig = {
3729
3795
  defaults: {
3730
3796
  name: "Transform",
@@ -3774,12 +3840,17 @@ var TransformConfig = {
3774
3840
  mappings: {
3775
3841
  type: "array",
3776
3842
  required: false,
3777
- description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "an index picks one list element ({{products[].variants[0].price}}). With groupBy, set lineField: true on the mappings that make each line. " + 'When the target field holds a record id but you have a reference (an email, SHOP-123), add lookup: { "object": "product.product", "matchField": "default_code" } — ' + "the write uses the id of the one record whose matchField equals the value; no match, or more than one, fails that row. Odoo only for now."
3843
+ description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "an index picks one list element ({{products[].variants[0].price}}). With groupBy, set lineField: true on the mappings that make each line. " + 'When the target field holds a record id but you have a reference (an email, SHOP-123), add lookup: { "object": "product.product", "matchField": "default_code" } — ' + "the write uses the id of the one record whose matchField equals the value; no match, or more than one, fails that row. Odoo and QuickBooks Online (e.g. CustomerRef.value → Customer by DisplayName; " + "Line.SalesItemLineDetail.ItemRef.value → Item by Name). A QuickBooks transaction upsert matches on DocNumber unless upsertMatchKey says otherwise."
3778
3844
  },
3779
3845
  groupBy: {
3780
3846
  type: "json",
3781
3847
  required: false,
3782
- description: 'One record per key, with line items: { "key": "{{orders[].number}}", "into": "line_items" }. Rows whose key is the same become ONE record ' + "(its fields from the first row); mappings with lineField: true become a list of lines under `into` (Shopify line_items, QuickBooks Line, Odoo order_line). " + "Dotted line fields nest inside each line (SalesItemLineDetail.ItemRef.value)."
3848
+ description: "Records with line items; mappings with lineField: true become a list of lines under `into` (Shopify line_items, QuickBooks Line, Odoo order_line). " + 'Two ways, combinable: (1) the lines are flat rows — { "key": "{{orders[].number}}", "into": "order_line" }: rows with the same key become ONE record ' + '(its fields from the first row), one line per row; (2) the lines are a list INSIDE each row — { "into": "order_line", "linesFrom": "{{orders[].line_items}}" }: ' + "each row is one record and each element of the list one line; a line field under the list ({{orders[].line_items[].quantity}}) reads the element, " + "any other path reads the row. Dotted line fields nest inside each line (SalesItemLineDetail.ItemRef.value)."
3849
+ },
3850
+ unwind: {
3851
+ type: "json",
3852
+ required: false,
3853
+ description: 'One row per element of a list on each row: { "path": "{{orders[].line_items}}", "keepEmpty": false }. Every mapping then reads the element under the ' + "list path ({{orders[].line_items[].sku}}) and the row everywhere else ({{orders[].name}}) — an order becomes one row per line, for targets that take " + "flat rows (Sheets, SQL). A row whose list is empty is dropped unless keepEmpty is true."
3783
3854
  },
3784
3855
  expression: { type: "code", required: false }
3785
3856
  },
@@ -3825,16 +3896,7 @@ var TransformConfig = {
3825
3896
  });
3826
3897
  }
3827
3898
  }
3828
- const groupBy = config.groupBy;
3829
- if (groupBy && (groupBy.key || groupBy.into)) {
3830
- if (!groupBy.key)
3831
- errors.push("One record per key: choose the key field (groupBy.key)");
3832
- if (!groupBy.into)
3833
- errors.push("One record per key: name the field that holds the lines (groupBy.into)");
3834
- if (groupBy.key && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
3835
- errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
3836
- }
3837
- }
3899
+ errors.push(...groupByErrors(config), ...unwindErrors(config));
3838
3900
  return { valid: errors.length === 0, errors };
3839
3901
  },
3840
3902
  merge: (userConfig) => {
@@ -4598,10 +4660,10 @@ var FILE_EDIT_SCHEMA = {
4598
4660
  matchOp: { type: "select", required: false, options: FILE_EDIT_MATCH_OPS, description: 'set_fields: comparison; "=" by default' },
4599
4661
  matchValue: { type: "string", required: false, description: "set_fields: value compared against" },
4600
4662
  setFields: { type: "object", required: false, description: "set_fields: { column: newValue } applied to matching rows" },
4601
- destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites an S3 source' },
4663
+ destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites the source file' },
4602
4664
  outputFileName: { type: "string", required: false, description: 'Name of the edited copy when destination is "new"' },
4603
4665
  source: { type: "select", required: false, options: FILE_EDIT_SOURCES, description: "Where the file to edit comes from; inferred from content when absent" },
4604
- s3Key: { type: "string", required: false, description: 'source "s3": platform-bucket key of the file to edit' },
4666
+ s3Key: { type: "string", required: false, description: `source "s3": an earlier step's file (its s3Key output) to edit` },
4605
4667
  url: { type: "url", required: false, description: 'source "url": the file to edit is fetched from here' }
4606
4668
  };
4607
4669
  function fileEditErrors(config) {
@@ -4633,7 +4695,7 @@ var CsvActionConfig = {
4633
4695
  },
4634
4696
  schema: {
4635
4697
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (CSV → records), generate (records → CSV) or edit an existing file" },
4636
- content: { type: "text", required: false, description: "CSV text, or an FX token from an upstream step" },
4698
+ content: { type: "text", required: false, description: "CSV text, or a reference to an earlier step's file content" },
4637
4699
  delimiter: { type: "select", required: false, options: [",", ";", "\t", "|"] },
4638
4700
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4639
4701
  encoding: { type: "string", required: false, description: "Text encoding (utf-8 by default)" },
@@ -4659,7 +4721,7 @@ var JsonActionConfig = {
4659
4721
  schema: {
4660
4722
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (JSON → records), generate (records → JSON) or edit an existing file" },
4661
4723
  format: { type: "select", required: false, options: ["json", "jsonl"], description: "json (array) or jsonl (one object per line)" },
4662
- content: { type: "text", required: false, description: "JSON text, or an FX token from an upstream step" },
4724
+ content: { type: "text", required: false, description: "JSON text, or a reference to an earlier step's file content" },
4663
4725
  prettyPrint: { type: "boolean", required: false, description: "generate: indent the output" },
4664
4726
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
4665
4727
  jsonl: { type: "boolean", required: false, description: "edit: treat the file as JSONL" },
@@ -4686,7 +4748,7 @@ var XmlActionConfig = {
4686
4748
  },
4687
4749
  schema: {
4688
4750
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (XML → records), generate (records → XML) or edit an existing file" },
4689
- content: { type: "text", required: false, description: "XML text, or an FX token from an upstream step" },
4751
+ content: { type: "text", required: false, description: "XML text, or a reference to an earlier step's file content" },
4690
4752
  rootElement: { type: "string", required: false, description: "generate: root element name" },
4691
4753
  rowElement: { type: "string", required: false, description: "Element that holds one record" },
4692
4754
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4708,7 +4770,7 @@ var ExcelActionConfig = {
4708
4770
  },
4709
4771
  schema: {
4710
4772
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (Excel → records), generate (records → Excel) or edit an existing file" },
4711
- content: { type: "text", required: false, description: "Platform-bucket key of the workbook, base64 bytes, or an FX token from an upstream step" },
4773
+ content: { type: "text", required: false, description: "A reference to an earlier step's Excel file (its s3Key output), or base64 bytes" },
4712
4774
  sheetName: { type: "string", required: false, description: "Sheet to read; the first sheet when absent" },
4713
4775
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4714
4776
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4726,7 +4788,7 @@ var FIXED_SCHEMA = {
4726
4788
  connector: { type: "connection", required: true, description: "Storage connector instance; its provider decides the operations" },
4727
4789
  operation: { type: "select", required: true, description: "Per provider: list, download, upload, delete, copy, move, presign, share, create_folder, transfer, sync" },
4728
4790
  outputAs: { type: "string", required: false, description: "Name of this step's output in the run's data bag" },
4729
- content: { type: "text", required: false, description: "upload: the body" },
4791
+ content: { type: "text", required: false, description: "upload: the file content; blank uploads the file the step before produced" },
4730
4792
  destConnector: { type: "connection", required: false, description: "transfer / sync: destination connector instance" },
4731
4793
  destPath: { type: "string", required: false, description: "transfer / sync: destination path; also move / copy destination on path-based providers" },
4732
4794
  sourceIsDirectory: { type: "boolean", required: false, description: "transfer: copy a folder recursively" },
package/dist/index.d.ts CHANGED
@@ -36,10 +36,11 @@ export { EmailConfig, } from "./catalog/communication/email-send/config";
36
36
  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
- export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_MODES, TRANSFORM_OPERATIONS, TransformConfig, type TransformConfigType, type TransformGroupBy, type TransformLookup, type TransformOperation, } from "./catalog/general/transform/config";
39
+ export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_MODES, TRANSFORM_OPERATIONS, TransformConfig, type TransformConfigType, type TransformGroupBy, type TransformLookup, type TransformUnwind, type TransformOperation, } from "./catalog/general/transform/config";
40
40
  export { AGGREGATE_FUNCTIONS, AGGREGATE_FUNCTION_DESCRIPTIONS, AggregateConfig, type AggregateConfigType, type AggregateFn, type AggregateMetric, type AggregateSchemaNode, groupKeyName, } from "./catalog/general/aggregate/config";
41
41
  export { detectRecordFields, resolveRecordSchema, type RecordSchemaLike, } from "./catalog/logic/for-each/recordSchema";
42
42
  export { STORAGE_CONNECTOR_IDS, type StorageExtraField, type StoragePathField, getStorageProvider, storageConfigKeys, } from "./catalog/storage/storageCapabilities";
43
+ export { FILE_OUTPUT_HELP, FILE_INPUT_HELP, STORAGE_DOWNLOAD_OUTPUT_SCHEMA, } from "./catalog/storage/fileHelp";
43
44
  export { ManualTriggerConfig } from "./catalog/triggers/manual-trigger/index";
44
45
  export { AiAgentConfig, type AiAgentConfigType, type AiAgentOverrides } from "./catalog/ai/ai-agent/config";
45
46
  export { IdpExtractConfig, IDP_EXTRACT_STRATEGIES, type IdpExtractConfigType } from "./catalog/ai/idp-extract/config";
package/dist/index.js CHANGED
@@ -1626,6 +1626,32 @@ var aggregateDefinition = {
1626
1626
  testable: false
1627
1627
  };
1628
1628
 
1629
+ // src/catalog/storage/fileHelp.ts
1630
+ var FILE_OUTPUT_HELP = {
1631
+ operation: "The operation that ran",
1632
+ data: "File contents, for text files such as CSV, JSON or TXT",
1633
+ s3Key: "The file itself, for PDF, Excel, images and other non-text files. Pass it to Excel, CSV, IDP or an Upload step.",
1634
+ fileName: "File name",
1635
+ mimeType: "File type, e.g. text/csv or application/pdf",
1636
+ path: "Where the file was read from"
1637
+ };
1638
+ var FILE_INPUT_HELP = {
1639
+ uploadContent: "Leave blank to upload the file from the step before, or pick a value from an earlier step, or type the content.",
1640
+ formatContent: (format) => `The ${format} from an earlier step (a download or a generated file), or paste the ${format} here.`,
1641
+ excelContent: "The Excel file from an earlier step (a download or a generated file)."
1642
+ };
1643
+ var STORAGE_DOWNLOAD_OUTPUT_SCHEMA = {
1644
+ type: "object",
1645
+ properties: {
1646
+ operation: { type: "string", description: FILE_OUTPUT_HELP.operation },
1647
+ data: { type: "string", description: FILE_OUTPUT_HELP.data },
1648
+ s3Key: { type: "string", description: FILE_OUTPUT_HELP.s3Key },
1649
+ fileName: { type: "string", description: FILE_OUTPUT_HELP.fileName },
1650
+ mimeType: { type: "string", description: FILE_OUTPUT_HELP.mimeType },
1651
+ path: { type: "string", description: FILE_OUTPUT_HELP.path }
1652
+ }
1653
+ };
1654
+
1629
1655
  // src/catalog/storage/csv-action/index.ts
1630
1656
  var csvActionDefinition = {
1631
1657
  id: "csv_action",
@@ -1640,7 +1666,7 @@ var csvActionDefinition = {
1640
1666
  inputSchema: {
1641
1667
  fields: [
1642
1668
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (CSV → Records)", value: "parse" }, { label: "Generate (Records → CSV)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1643
- { key: "content", label: "CSV Content", type: "string" /* String */, description: "Paste CSV content or use FX token from upstream step" },
1669
+ { key: "content", label: "CSV Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("CSV") },
1644
1670
  { key: "delimiter", label: "Delimiter", type: "select" /* Select */, options: [{ label: "Comma (,)", value: "," }, { label: "Semicolon (;)", value: ";" }, { label: "Tab", value: "\t" }, { label: "Pipe (|)", value: "|" }], defaultValue: "," },
1645
1671
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing column headers (default: 1)", defaultValue: 1 },
1646
1672
  { key: "encoding", label: "Encoding", type: "select" /* Select */, options: [{ label: "UTF-8", value: "utf-8" }, { label: "Latin-1", value: "latin1" }, { label: "ASCII", value: "ascii" }], defaultValue: "utf-8" },
@@ -1680,7 +1706,7 @@ var jsonActionDefinition = {
1680
1706
  fields: [
1681
1707
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (JSON → Records)", value: "parse" }, { label: "Generate (Records → JSON)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1682
1708
  { key: "format", label: "Format", type: "select" /* Select */, options: [{ label: "JSON (array)", value: "json" }, { label: "JSONL (one object per line)", value: "jsonl" }], defaultValue: "json" },
1683
- { key: "content", label: "JSON Content", type: "string" /* String */, description: "Paste JSON content or use FX token from upstream step" },
1709
+ { key: "content", label: "JSON Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("JSON") },
1684
1710
  { key: "prettyPrint", label: "Pretty Print", type: "boolean" /* Boolean */, description: "Format output with indentation", defaultValue: true },
1685
1711
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.json" }
1686
1712
  ]
@@ -1716,7 +1742,7 @@ var xmlActionDefinition = {
1716
1742
  inputSchema: {
1717
1743
  fields: [
1718
1744
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (XML → Records)", value: "parse" }, { label: "Generate (Records → XML)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1719
- { key: "content", label: "XML Content", type: "string" /* String */, description: "Paste XML or use FX token" },
1745
+ { key: "content", label: "XML Content", type: "string" /* String */, description: FILE_INPUT_HELP.formatContent("XML") },
1720
1746
  { key: "rootElement", label: "Root Element", type: "string" /* String */, description: "Root XML element name (for generate)", defaultValue: "data" },
1721
1747
  { key: "rowElement", label: "Row Element", type: "string" /* String */, description: "Element name per record (for generate)", defaultValue: "row" },
1722
1748
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xml" }
@@ -1753,7 +1779,7 @@ var excelActionDefinition = {
1753
1779
  inputSchema: {
1754
1780
  fields: [
1755
1781
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Parse (Excel → Records)", value: "parse" }, { label: "Generate (Records → Excel)", value: "generate" }, { label: "Edit existing file", value: "edit" }], defaultValue: "parse", validation: { required: true } },
1756
- { key: "content", label: "Excel File", type: "string" /* String */, description: "S3 key of uploaded Excel file or FX token from upstream" },
1782
+ { key: "content", label: "Excel File", type: "string" /* String */, description: FILE_INPUT_HELP.excelContent },
1757
1783
  { key: "sheetName", label: "Sheet Name", type: "string" /* String */, description: "Name of sheet to read (default: first sheet)" },
1758
1784
  { key: "headerRow", label: "Header Row", type: "number" /* Number */, description: "Row number containing headers", defaultValue: 1 },
1759
1785
  { key: "fileName", label: "Output File Name", type: "string" /* String */, defaultValue: "export.xlsx" }
@@ -1765,7 +1791,7 @@ var excelActionDefinition = {
1765
1791
  { key: "data", type: "array" /* Array */, description: "Parsed records (parse mode) — loop these with a ForEach. File metadata in generate mode." },
1766
1792
  { key: "columns", type: "json" /* JSON */, description: "Column names" },
1767
1793
  { key: "rowCount", type: "number" /* Number */, description: "Number of records" },
1768
- { key: "s3Key", type: "string" /* String */, description: "S3 key of generated file" }
1794
+ { key: "s3Key", type: "string" /* String */, description: "The generated Excel file. Pass it to an Upload step to save it." }
1769
1795
  ],
1770
1796
  sampleOutput: { data: [{ id: 1, name: "Widget" }], columns: ["id", "name"], rowCount: 1 }
1771
1797
  },
@@ -1795,7 +1821,7 @@ var s3ActionDefinition = {
1795
1821
  { key: "bucket", label: "Bucket Name", type: "string" /* String */, description: "S3 bucket name (defaults to the connector’s bucket)" },
1796
1822
  { key: "key", label: "Object Key (Path)", type: "string" /* String */, description: "S3 object key, e.g. data/reports/monthly.csv" },
1797
1823
  { key: "prefix", label: "Prefix (for List)", type: "string" /* String */, description: "Filter objects by prefix (folder path)" },
1798
- { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
1824
+ { key: "content", label: "File Content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1799
1825
  { key: "contentType", label: "Content Type", type: "string" /* String */, description: "MIME type (e.g. text/csv, application/json)", defaultValue: "application/octet-stream" },
1800
1826
  { key: "destBucket", label: "Destination Bucket (for Copy)", type: "string" /* String */ },
1801
1827
  { key: "destKey", label: "Destination Key (for Copy)", type: "string" /* String */ },
@@ -1836,7 +1862,7 @@ var ftpActionDefinition = {
1836
1862
  { key: "connector", label: "FTP / SFTP Connection", type: "connection" /* Connection */, description: "Select a configured FTP/SFTP connector from Integrations", validation: { required: true } },
1837
1863
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete File", value: "delete" }, { label: "Move / Rename", value: "move" }], defaultValue: "download", validation: { required: true } },
1838
1864
  { key: "remotePath", label: "Remote Path", type: "string" /* String */, description: "File or folder path on the server (use file explorer to browse)" },
1839
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream step" },
1865
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1840
1866
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */, description: "Name for the uploaded file" },
1841
1867
  { key: "destPath", label: "Destination Path (Move)", type: "string" /* String */, description: "New path for moved/renamed file" },
1842
1868
  { key: "ifExists", label: "If File Exists", type: "select" /* Select */, options: [{ label: "Overwrite", value: "overwrite" }, { label: "Skip", value: "skip" }], defaultValue: "overwrite" }
@@ -1877,7 +1903,7 @@ var googleDriveActionDefinition = {
1877
1903
  { key: "fileId", label: "File ID / Path", type: "string" /* String */, description: "Google Drive file ID (use file explorer to browse)" },
1878
1904
  { key: "folderId", label: "Folder ID", type: "string" /* String */, description: "Target folder (root if empty)" },
1879
1905
  { key: "fileName", label: "File Name", type: "string" /* String */ },
1880
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
1906
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1881
1907
  { key: "mimeType", label: "MIME Type", type: "string" /* String */, defaultValue: "application/octet-stream" },
1882
1908
  { key: "destFolderId", label: "Destination Folder (Move/Copy)", type: "string" /* String */ },
1883
1909
  { key: "shareEmail", label: "Share With Email", type: "string" /* String */ },
@@ -1919,7 +1945,7 @@ var dropboxActionDefinition = {
1919
1945
  { key: "connector", label: "Dropbox Account", type: "connection" /* Connection */, description: "Select a connected Dropbox account", validation: { required: true } },
1920
1946
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }, { label: "Create Folder", value: "create_folder" }, { label: "Get Shared Link", value: "shared_link" }], defaultValue: "download", validation: { required: true } },
1921
1947
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on Dropbox (use file explorer to browse)" },
1922
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
1948
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1923
1949
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
1924
1950
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ },
1925
1951
  { key: "mode", label: "Upload Mode", type: "select" /* Select */, options: [{ label: "Add (fail if exists)", value: "add" }, { label: "Overwrite", value: "overwrite" }], defaultValue: "overwrite" }
@@ -1959,7 +1985,7 @@ var onedriveActionDefinition = {
1959
1985
  { key: "operation", label: "Operation", type: "select" /* Select */, options: [{ label: "Download File", value: "download" }, { label: "Upload File", value: "upload" }, { label: "List Files", value: "list" }, { label: "Delete", value: "delete" }, { label: "Move", value: "move" }, { label: "Copy", value: "copy" }], defaultValue: "download", validation: { required: true } },
1960
1986
  { key: "path", label: "File / Folder Path", type: "string" /* String */, description: "Path on OneDrive (use file explorer to browse)" },
1961
1987
  { key: "itemId", label: "Item ID", type: "string" /* String */, description: "OneDrive item ID (alternative to path)" },
1962
- { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: "Content to upload — use FX token from upstream" },
1988
+ { key: "content", label: "File Content (Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent },
1963
1989
  { key: "fileName", label: "File Name (Upload)", type: "string" /* String */ },
1964
1990
  { key: "destPath", label: "Destination Path (Move/Copy)", type: "string" /* String */ }
1965
1991
  ]
@@ -2017,14 +2043,17 @@ var storageActionDefinition = {
2017
2043
  },
2018
2044
  { key: "path", label: "Path", type: "string" /* String */, description: "File or folder path — use the file browser in the panel." },
2019
2045
  { key: "destPath", label: "Destination path", type: "string" /* String */, description: "Target for copy / move." },
2020
- { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: "Content to upload — bind an FX token from an upstream step." }
2046
+ { key: "content", label: "File content (for Upload)", type: "string" /* String */, description: FILE_INPUT_HELP.uploadContent }
2021
2047
  ]
2022
2048
  },
2023
2049
  outputSchema: {
2024
2050
  type: "object",
2025
2051
  properties: [
2026
2052
  { key: "operation", type: "string" /* String */, description: "The operation that ran" },
2027
- { key: "data", type: "json" /* JSON */, description: "Operation result or downloaded content" },
2053
+ { key: "data", type: "json" /* JSON */, description: `Download: ${FILE_OUTPUT_HELP.data}. Other operations: their result.` },
2054
+ { key: "s3Key", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.s3Key}` },
2055
+ { key: "fileName", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.fileName}` },
2056
+ { key: "mimeType", type: "string" /* String */, description: `Download: ${FILE_OUTPUT_HELP.mimeType}` },
2028
2057
  { key: "files", type: "json" /* JSON */, description: "Listing (for the List operation)" },
2029
2058
  { key: "path", type: "string" /* String */, description: "Affected file/folder path" },
2030
2059
  { key: "url", type: "string" /* String */, description: "Signed URL (for the Get signed URL operation)" }
@@ -3182,7 +3211,13 @@ var ForEachConfig = {
3182
3211
  itemVariable: { type: "string", required: true },
3183
3212
  indexVariable: { type: "string", required: false },
3184
3213
  batchSize: { type: "number", required: false },
3185
- continueOnError: { type: "boolean", required: false }
3214
+ continueOnError: { type: "boolean", required: false },
3215
+ collect: {
3216
+ type: "select",
3217
+ required: false,
3218
+ options: ["array", "none", "groupBy", "reduce", "rows"],
3219
+ description: "What the Done port carries besides the counts: array (each iteration's result in `results`, the default), none, groupBy (by groupByPath), " + "reduce (reduceOp over reducePath), or rows — also `data`: every row the body produced (each iteration's Query/Transform rows, else its result), " + "so ONE step on the Done port maps and writes them all instead of a write per iteration."
3220
+ }
3186
3221
  },
3187
3222
  validate: (config) => {
3188
3223
  const errors = [];
@@ -3567,6 +3602,34 @@ var TRANSFORM_OPERATIONS = [
3567
3602
  ];
3568
3603
  var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
3569
3604
  var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "google_sheets"]);
3605
+ function groupByErrors(config) {
3606
+ const groupBy = config.groupBy;
3607
+ if (!groupBy || !(groupBy.key || groupBy.into || groupBy.linesFrom))
3608
+ return [];
3609
+ const errors = [];
3610
+ if (!groupBy.key && !groupBy.linesFrom) {
3611
+ errors.push("One record per key: choose the key field (groupBy.key)");
3612
+ }
3613
+ if (!groupBy.into)
3614
+ errors.push("One record per key: name the field that holds the lines (groupBy.into)");
3615
+ if ((groupBy.key || groupBy.linesFrom) && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
3616
+ errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
3617
+ }
3618
+ return errors;
3619
+ }
3620
+ function unwindErrors(config) {
3621
+ const unwind = config.unwind;
3622
+ if (!unwind)
3623
+ return [];
3624
+ if (!unwind.path?.trim()) {
3625
+ return unwind.keepEmpty !== undefined ? ["One row per item: choose the list (unwind.path)"] : [];
3626
+ }
3627
+ const linesFrom = config.groupBy?.linesFrom;
3628
+ if (linesFrom && linesFrom.trim() === unwind.path.trim()) {
3629
+ return ["One row per item and Lines come from name the same list — use one of them (unwind.path, groupBy.linesFrom)"];
3630
+ }
3631
+ return [];
3632
+ }
3570
3633
  var TransformConfig = {
3571
3634
  defaults: {
3572
3635
  name: "Transform",
@@ -3616,12 +3679,17 @@ var TransformConfig = {
3616
3679
  mappings: {
3617
3680
  type: "array",
3618
3681
  required: false,
3619
- description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "an index picks one list element ({{products[].variants[0].price}}). With groupBy, set lineField: true on the mappings that make each line. " + 'When the target field holds a record id but you have a reference (an email, SHOP-123), add lookup: { "object": "product.product", "matchField": "default_code" } — ' + "the write uses the id of the one record whose matchField equals the value; no match, or more than one, fails that row. Odoo only for now."
3682
+ description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "an index picks one list element ({{products[].variants[0].price}}). With groupBy, set lineField: true on the mappings that make each line. " + 'When the target field holds a record id but you have a reference (an email, SHOP-123), add lookup: { "object": "product.product", "matchField": "default_code" } — ' + "the write uses the id of the one record whose matchField equals the value; no match, or more than one, fails that row. Odoo and QuickBooks Online (e.g. CustomerRef.value → Customer by DisplayName; " + "Line.SalesItemLineDetail.ItemRef.value → Item by Name). A QuickBooks transaction upsert matches on DocNumber unless upsertMatchKey says otherwise."
3620
3683
  },
3621
3684
  groupBy: {
3622
3685
  type: "json",
3623
3686
  required: false,
3624
- description: 'One record per key, with line items: { "key": "{{orders[].number}}", "into": "line_items" }. Rows whose key is the same become ONE record ' + "(its fields from the first row); mappings with lineField: true become a list of lines under `into` (Shopify line_items, QuickBooks Line, Odoo order_line). " + "Dotted line fields nest inside each line (SalesItemLineDetail.ItemRef.value)."
3687
+ description: "Records with line items; mappings with lineField: true become a list of lines under `into` (Shopify line_items, QuickBooks Line, Odoo order_line). " + 'Two ways, combinable: (1) the lines are flat rows — { "key": "{{orders[].number}}", "into": "order_line" }: rows with the same key become ONE record ' + '(its fields from the first row), one line per row; (2) the lines are a list INSIDE each row — { "into": "order_line", "linesFrom": "{{orders[].line_items}}" }: ' + "each row is one record and each element of the list one line; a line field under the list ({{orders[].line_items[].quantity}}) reads the element, " + "any other path reads the row. Dotted line fields nest inside each line (SalesItemLineDetail.ItemRef.value)."
3688
+ },
3689
+ unwind: {
3690
+ type: "json",
3691
+ required: false,
3692
+ description: 'One row per element of a list on each row: { "path": "{{orders[].line_items}}", "keepEmpty": false }. Every mapping then reads the element under the ' + "list path ({{orders[].line_items[].sku}}) and the row everywhere else ({{orders[].name}}) — an order becomes one row per line, for targets that take " + "flat rows (Sheets, SQL). A row whose list is empty is dropped unless keepEmpty is true."
3625
3693
  },
3626
3694
  expression: { type: "code", required: false }
3627
3695
  },
@@ -3667,16 +3735,7 @@ var TransformConfig = {
3667
3735
  });
3668
3736
  }
3669
3737
  }
3670
- const groupBy = config.groupBy;
3671
- if (groupBy && (groupBy.key || groupBy.into)) {
3672
- if (!groupBy.key)
3673
- errors.push("One record per key: choose the key field (groupBy.key)");
3674
- if (!groupBy.into)
3675
- errors.push("One record per key: name the field that holds the lines (groupBy.into)");
3676
- if (groupBy.key && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
3677
- errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
3678
- }
3679
- }
3738
+ errors.push(...groupByErrors(config), ...unwindErrors(config));
3680
3739
  return { valid: errors.length === 0, errors };
3681
3740
  },
3682
3741
  merge: (userConfig) => {
@@ -4440,10 +4499,10 @@ var FILE_EDIT_SCHEMA = {
4440
4499
  matchOp: { type: "select", required: false, options: FILE_EDIT_MATCH_OPS, description: 'set_fields: comparison; "=" by default' },
4441
4500
  matchValue: { type: "string", required: false, description: "set_fields: value compared against" },
4442
4501
  setFields: { type: "object", required: false, description: "set_fields: { column: newValue } applied to matching rows" },
4443
- destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites an S3 source' },
4502
+ destination: { type: "select", required: false, options: FILE_EDIT_DESTINATIONS, description: '"new" writes an edited copy (default); "in_place" overwrites the source file' },
4444
4503
  outputFileName: { type: "string", required: false, description: 'Name of the edited copy when destination is "new"' },
4445
4504
  source: { type: "select", required: false, options: FILE_EDIT_SOURCES, description: "Where the file to edit comes from; inferred from content when absent" },
4446
- s3Key: { type: "string", required: false, description: 'source "s3": platform-bucket key of the file to edit' },
4505
+ s3Key: { type: "string", required: false, description: `source "s3": an earlier step's file (its s3Key output) to edit` },
4447
4506
  url: { type: "url", required: false, description: 'source "url": the file to edit is fetched from here' }
4448
4507
  };
4449
4508
  function fileEditErrors(config) {
@@ -4475,7 +4534,7 @@ var CsvActionConfig = {
4475
4534
  },
4476
4535
  schema: {
4477
4536
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (CSV → records), generate (records → CSV) or edit an existing file" },
4478
- content: { type: "text", required: false, description: "CSV text, or an FX token from an upstream step" },
4537
+ content: { type: "text", required: false, description: "CSV text, or a reference to an earlier step's file content" },
4479
4538
  delimiter: { type: "select", required: false, options: [",", ";", "\t", "|"] },
4480
4539
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4481
4540
  encoding: { type: "string", required: false, description: "Text encoding (utf-8 by default)" },
@@ -4501,7 +4560,7 @@ var JsonActionConfig = {
4501
4560
  schema: {
4502
4561
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (JSON → records), generate (records → JSON) or edit an existing file" },
4503
4562
  format: { type: "select", required: false, options: ["json", "jsonl"], description: "json (array) or jsonl (one object per line)" },
4504
- content: { type: "text", required: false, description: "JSON text, or an FX token from an upstream step" },
4563
+ content: { type: "text", required: false, description: "JSON text, or a reference to an earlier step's file content" },
4505
4564
  prettyPrint: { type: "boolean", required: false, description: "generate: indent the output" },
4506
4565
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
4507
4566
  jsonl: { type: "boolean", required: false, description: "edit: treat the file as JSONL" },
@@ -4528,7 +4587,7 @@ var XmlActionConfig = {
4528
4587
  },
4529
4588
  schema: {
4530
4589
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (XML → records), generate (records → XML) or edit an existing file" },
4531
- content: { type: "text", required: false, description: "XML text, or an FX token from an upstream step" },
4590
+ content: { type: "text", required: false, description: "XML text, or a reference to an earlier step's file content" },
4532
4591
  rootElement: { type: "string", required: false, description: "generate: root element name" },
4533
4592
  rowElement: { type: "string", required: false, description: "Element that holds one record" },
4534
4593
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4550,7 +4609,7 @@ var ExcelActionConfig = {
4550
4609
  },
4551
4610
  schema: {
4552
4611
  operation: { type: "select", required: true, options: FILE_FORMAT_OPERATIONS, description: "parse (Excel → records), generate (records → Excel) or edit an existing file" },
4553
- content: { type: "text", required: false, description: "Platform-bucket key of the workbook, base64 bytes, or an FX token from an upstream step" },
4612
+ content: { type: "text", required: false, description: "A reference to an earlier step's Excel file (its s3Key output), or base64 bytes" },
4554
4613
  sheetName: { type: "string", required: false, description: "Sheet to read; the first sheet when absent" },
4555
4614
  headerRow: { type: "number", required: false, min: 1, description: "1-based row that holds the column headers" },
4556
4615
  fileName: { type: "string", required: false, description: "generate: name of the produced file" },
@@ -4568,7 +4627,7 @@ var FIXED_SCHEMA = {
4568
4627
  connector: { type: "connection", required: true, description: "Storage connector instance; its provider decides the operations" },
4569
4628
  operation: { type: "select", required: true, description: "Per provider: list, download, upload, delete, copy, move, presign, share, create_folder, transfer, sync" },
4570
4629
  outputAs: { type: "string", required: false, description: "Name of this step's output in the run's data bag" },
4571
- content: { type: "text", required: false, description: "upload: the body" },
4630
+ content: { type: "text", required: false, description: "upload: the file content; blank uploads the file the step before produced" },
4572
4631
  destConnector: { type: "connection", required: false, description: "transfer / sync: destination connector instance" },
4573
4632
  destPath: { type: "string", required: false, description: "transfer / sync: destination path; also move / copy destination on path-based providers" },
4574
4633
  sourceIsDirectory: { type: "boolean", required: false, description: "transfer: copy a folder recursively" },
@@ -5972,6 +6031,8 @@ export {
5972
6031
  FILE_EDIT_SCHEMA,
5973
6032
  FILE_EDIT_SOURCES,
5974
6033
  FILE_FORMAT_OPERATIONS,
6034
+ FILE_INPUT_HELP,
6035
+ FILE_OUTPUT_HELP,
5975
6036
  FTP_OPERATIONS,
5976
6037
  FieldType,
5977
6038
  ForEachConfig,
@@ -5997,6 +6058,7 @@ export {
5997
6058
  S3ActionConfig,
5998
6059
  S3_OPERATIONS,
5999
6060
  STORAGE_CONNECTOR_IDS,
6061
+ STORAGE_DOWNLOAD_OUTPUT_SCHEMA,
6000
6062
  SortDirection,
6001
6063
  StorageActionConfig,
6002
6064
  TRANSFORM_MODES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octaviaflow/flow-rules",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
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",