@octaviaflow/flow-rules 0.21.0 → 0.22.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.
@@ -67,6 +67,18 @@ export interface TransformUnwind {
67
67
  /** Keep a row whose list is empty, as one row with no element. Default false. */
68
68
  keepEmpty?: boolean;
69
69
  }
70
+ /** How rows are written — the Transform panel's "Advanced options" (DEV-267/268). */
71
+ export interface TransformWriteSettings {
72
+ /** Rows per write call. Empty = the connector's own default. */
73
+ batchSize?: number;
74
+ /** `continue` (default): failed rows are reported and the rest are written. `fail`: the step stops at the first failed row. */
75
+ onError?: 'continue' | 'fail';
76
+ /** Map and validate, write nothing. */
77
+ dryRun?: boolean;
78
+ /** Put the failed rows on the step's output. */
79
+ exposeFailedRows?: boolean;
80
+ }
81
+ export declare const TRANSFORM_ON_ERROR: readonly NonNullable<TransformWriteSettings['onError']>[];
70
82
  export type TransformMode = 'visual' | 'expression';
71
83
  /**
72
84
  * The authoring modes. `visual` is the Field Mapper and the only mode the panel
@@ -111,6 +123,8 @@ export interface TransformConfigType {
111
123
  groupBy?: TransformGroupBy;
112
124
  /** One row per element of a list on each row, before mapping (DEV-484). */
113
125
  unwind?: TransformUnwind;
126
+ /** How rows are written (the panel's "Advanced options", DEV-267/268). */
127
+ writeSettings?: TransformWriteSettings;
114
128
  /** Ordered column header names — the exact columns + header text written. */
115
129
  sheetHeaders?: string[];
116
130
  /** Create the target tab on first write if it doesn't exist (default true). */
@@ -139,6 +153,16 @@ export declare const TransformConfig: {
139
153
  options: readonly TransformOperation[];
140
154
  description: string;
141
155
  };
156
+ upsertMatchKey: {
157
+ type: string;
158
+ required: boolean;
159
+ description: string;
160
+ };
161
+ writeSettings: {
162
+ type: string;
163
+ required: boolean;
164
+ description: string;
165
+ };
142
166
  connectorId: {
143
167
  type: string;
144
168
  required: boolean;
package/dist/index.cjs CHANGED
@@ -107,6 +107,7 @@ __export(exports_src, {
107
107
  SortDirection: () => SortDirection,
108
108
  StorageActionConfig: () => StorageActionConfig,
109
109
  TRANSFORM_MODES: () => TRANSFORM_MODES,
110
+ TRANSFORM_ON_ERROR: () => TRANSFORM_ON_ERROR,
110
111
  TRANSFORM_OPERATIONS: () => TRANSFORM_OPERATIONS,
111
112
  TransformConfig: () => TransformConfig,
112
113
  USER_FUNCTION_PREFIX: () => USER_FUNCTION_PREFIX,
@@ -3387,6 +3388,7 @@ var WhatsappSendConfig = {
3387
3388
  };
3388
3389
 
3389
3390
  // src/catalog/general/transform/config.ts
3391
+ var TRANSFORM_ON_ERROR = ["continue", "fail"];
3390
3392
  var TRANSFORM_MODES = ["visual", "expression"];
3391
3393
  var TRANSFORM_OPERATIONS = [
3392
3394
  { id: "upsert", label: "Upsert" },
@@ -3411,6 +3413,21 @@ function groupByErrors(config) {
3411
3413
  }
3412
3414
  return errors;
3413
3415
  }
3416
+ function writeSettingsErrors(config) {
3417
+ const ws = config.writeSettings;
3418
+ if (ws === undefined || ws === null)
3419
+ return [];
3420
+ if (typeof ws !== "object" || Array.isArray(ws))
3421
+ return ["Write settings must be an object (writeSettings)"];
3422
+ const errors = [];
3423
+ if (ws.onError !== undefined && !isOneOf(TRANSFORM_ON_ERROR, ws.onError)) {
3424
+ errors.push(`Write settings: onError must be one of: ${TRANSFORM_ON_ERROR.join(", ")} (got "${String(ws.onError)}")`);
3425
+ }
3426
+ if (ws.batchSize !== undefined && !(Number.isInteger(ws.batchSize) && ws.batchSize > 0)) {
3427
+ errors.push(`Write settings: batchSize must be a whole number above 0 (got "${String(ws.batchSize)}")`);
3428
+ }
3429
+ return errors;
3430
+ }
3414
3431
  function unwindErrors(config) {
3415
3432
  const unwind = config.unwind;
3416
3433
  if (!unwind)
@@ -3452,6 +3469,16 @@ var TransformConfig = {
3452
3469
  options: TRANSFORM_OPERATION_IDS,
3453
3470
  description: "What to do with each mapped record at the target: upsert, insert, update or delete"
3454
3471
  },
3472
+ upsertMatchKey: {
3473
+ type: "text",
3474
+ required: false,
3475
+ description: "For upsert and update: the TARGET field that says which existing record a row is (DEV-646). A row whose value matches a record updates it; " + "no match creates one (upsert). Empty = the connector decides: Salesforce Id, MongoDB _id, the SQL primary key. " + "A sync that may run more than once must upsert on a key that is stable and unique for the SOURCE record, or every run creates the records again. " + "Salesforce: the key must be an External ID field on the object (describe_connector_object) — mapping a source id into a normal field is not enough, and " + 'without one the upsert fails with "External ID is not found in record". If the object has no External ID field for this source, say so and ask the person: ' + "they create one in Salesforce (for example a HubSpot Contact Id text field marked External ID and Unique), or choose insert knowing re-runs duplicate."
3476
+ },
3477
+ writeSettings: {
3478
+ type: "json",
3479
+ required: false,
3480
+ description: 'How rows are written: { "batchSize"?: number, "onError"?: "continue" | "fail", "dryRun"?: boolean, "exposeFailedRows"?: boolean }. ' + 'onError "continue" (the default) writes every row it can and reports the failures (the run ends as partial success); "fail" stops the step at the first failed row. ' + "dryRun maps and validates without writing."
3481
+ },
3455
3482
  connectorId: { type: "connection", required: true },
3456
3483
  database: {
3457
3484
  type: "text",
@@ -3473,7 +3500,7 @@ var TransformConfig = {
3473
3500
  mappings: {
3474
3501
  type: "array",
3475
3502
  required: false,
3476
- 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."
3503
+ description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "targetField is the target app's own field name, normally one describe_connector_object lists; a name it does not list is written as given, so the field must exist in the target app (a custom field the person created). " + "A field the target requires (Salesforce Account Name) must be mapped, and a source value that can be empty needs a fallback or the row fails. " + "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."
3477
3504
  },
3478
3505
  groupBy: {
3479
3506
  type: "json",
@@ -3529,7 +3556,7 @@ var TransformConfig = {
3529
3556
  });
3530
3557
  }
3531
3558
  }
3532
- errors.push(...groupByErrors(config), ...unwindErrors(config));
3559
+ errors.push(...groupByErrors(config), ...unwindErrors(config), ...writeSettingsErrors(config));
3533
3560
  return { valid: errors.length === 0, errors };
3534
3561
  },
3535
3562
  merge: (userConfig) => {
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export { EmailConfig, } from "./catalog/communication/email-send/config";
36
36
  export { queryDefinition, } from "./catalog/general/query/index";
37
37
  export { isQueryableConnector, queryFieldsSatisfied, queryTablesSatisfied, validateBuildQueryConfig, validateVisualConfig, QUERY_AUTHORED_SQL_MESSAGE, QUERY_VISUAL_ONLY_MESSAGE, } from "./catalog/general/query/config";
38
38
  export { type BuildQueryConfig, type ColumnSchema, type CompiledQuery, type Condition, type ConditionGroup, type ConditionRow, type ConnectorSchema, isConditionGroup, MAX_CONDITION_DEPTH, QUERY_AGGREGATE_FUNCTIONS, QUERY_COMPARISON_OPERATORS, QUERY_CONDITION_OPERATORS, QUERY_MODES, QUERY_RELATIVE_DATE_OPERATORS, QUERY_TYPES, type QueryAggregate, type QueryAggregateFn, type QueryComparisonOperator, type QueryConditionOperator, type QueryExecutionResult, type QueryJoin, type QueryJoinOperator, type QuerySort, RELATIVE_DATE_DIRECTIONS, RELATIVE_DATE_MAX_AMOUNT, RELATIVE_DATE_UNITS, type RelativeDate, type RelativeDateDirection, type RelativeDateUnit, SortDirection, type VisualConfigV2, 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 TransformUnwind, type TransformOperation, } from "./catalog/general/transform/config";
39
+ export { DATABASE_CONNECTOR_IDS, type FieldMapping, TRANSFORM_MODES, TRANSFORM_ON_ERROR, TRANSFORM_OPERATIONS, TransformConfig, type TransformConfigType, type TransformGroupBy, type TransformLookup, type TransformUnwind, type TransformOperation, type TransformWriteSettings, } 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";
package/dist/index.js CHANGED
@@ -3210,6 +3210,7 @@ var WhatsappSendConfig = {
3210
3210
  };
3211
3211
 
3212
3212
  // src/catalog/general/transform/config.ts
3213
+ var TRANSFORM_ON_ERROR = ["continue", "fail"];
3213
3214
  var TRANSFORM_MODES = ["visual", "expression"];
3214
3215
  var TRANSFORM_OPERATIONS = [
3215
3216
  { id: "upsert", label: "Upsert" },
@@ -3234,6 +3235,21 @@ function groupByErrors(config) {
3234
3235
  }
3235
3236
  return errors;
3236
3237
  }
3238
+ function writeSettingsErrors(config) {
3239
+ const ws = config.writeSettings;
3240
+ if (ws === undefined || ws === null)
3241
+ return [];
3242
+ if (typeof ws !== "object" || Array.isArray(ws))
3243
+ return ["Write settings must be an object (writeSettings)"];
3244
+ const errors = [];
3245
+ if (ws.onError !== undefined && !isOneOf(TRANSFORM_ON_ERROR, ws.onError)) {
3246
+ errors.push(`Write settings: onError must be one of: ${TRANSFORM_ON_ERROR.join(", ")} (got "${String(ws.onError)}")`);
3247
+ }
3248
+ if (ws.batchSize !== undefined && !(Number.isInteger(ws.batchSize) && ws.batchSize > 0)) {
3249
+ errors.push(`Write settings: batchSize must be a whole number above 0 (got "${String(ws.batchSize)}")`);
3250
+ }
3251
+ return errors;
3252
+ }
3237
3253
  function unwindErrors(config) {
3238
3254
  const unwind = config.unwind;
3239
3255
  if (!unwind)
@@ -3275,6 +3291,16 @@ var TransformConfig = {
3275
3291
  options: TRANSFORM_OPERATION_IDS,
3276
3292
  description: "What to do with each mapped record at the target: upsert, insert, update or delete"
3277
3293
  },
3294
+ upsertMatchKey: {
3295
+ type: "text",
3296
+ required: false,
3297
+ description: "For upsert and update: the TARGET field that says which existing record a row is (DEV-646). A row whose value matches a record updates it; " + "no match creates one (upsert). Empty = the connector decides: Salesforce Id, MongoDB _id, the SQL primary key. " + "A sync that may run more than once must upsert on a key that is stable and unique for the SOURCE record, or every run creates the records again. " + "Salesforce: the key must be an External ID field on the object (describe_connector_object) — mapping a source id into a normal field is not enough, and " + 'without one the upsert fails with "External ID is not found in record". If the object has no External ID field for this source, say so and ask the person: ' + "they create one in Salesforce (for example a HubSpot Contact Id text field marked External ID and Unique), or choose insert knowing re-runs duplicate."
3298
+ },
3299
+ writeSettings: {
3300
+ type: "json",
3301
+ required: false,
3302
+ description: 'How rows are written: { "batchSize"?: number, "onError"?: "continue" | "fail", "dryRun"?: boolean, "exposeFailedRows"?: boolean }. ' + 'onError "continue" (the default) writes every row it can and reports the failures (the run ends as partial success); "fail" stops the step at the first failed row. ' + "dryRun maps and validates without writing."
3303
+ },
3278
3304
  connectorId: { type: "connection", required: true },
3279
3305
  database: {
3280
3306
  type: "text",
@@ -3296,7 +3322,7 @@ var TransformConfig = {
3296
3322
  mappings: {
3297
3323
  type: "array",
3298
3324
  required: false,
3299
- 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."
3325
+ description: "Field mappings: [{ id, sourceField, targetField, targetType, transform? }]. sourceField is a token like {{orders[].email}}; " + "targetField is the target app's own field name, normally one describe_connector_object lists; a name it does not list is written as given, so the field must exist in the target app (a custom field the person created). " + "A field the target requires (Salesforce Account Name) must be mapped, and a source value that can be empty needs a fallback or the row fails. " + "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."
3300
3326
  },
3301
3327
  groupBy: {
3302
3328
  type: "json",
@@ -3352,7 +3378,7 @@ var TransformConfig = {
3352
3378
  });
3353
3379
  }
3354
3380
  }
3355
- errors.push(...groupByErrors(config), ...unwindErrors(config));
3381
+ errors.push(...groupByErrors(config), ...unwindErrors(config), ...writeSettingsErrors(config));
3356
3382
  return { valid: errors.length === 0, errors };
3357
3383
  },
3358
3384
  merge: (userConfig) => {
@@ -6243,6 +6269,7 @@ export {
6243
6269
  SortDirection,
6244
6270
  StorageActionConfig,
6245
6271
  TRANSFORM_MODES,
6272
+ TRANSFORM_ON_ERROR,
6246
6273
  TRANSFORM_OPERATIONS,
6247
6274
  TransformConfig,
6248
6275
  USER_FUNCTION_PREFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octaviaflow/flow-rules",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "The flow action catalog and the rules engine behind Flow Doctor \u2014 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",