@octaviaflow/flow-rules 0.15.0 → 0.17.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/general/transform/config.d.ts +39 -3
- package/dist/catalog/logic/for-each/config.d.ts +7 -1
- package/dist/index.cjs +47 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +47 -13
- package/package.json +1 -1
|
@@ -26,6 +26,19 @@ export interface FieldMapping {
|
|
|
26
26
|
depth?: number;
|
|
27
27
|
/** With `groupBy`: this field goes into each line of the record's list, not the record (DEV-478). Root level only. */
|
|
28
28
|
lineField?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* "Find by" (DEV-480): the mapped value names a record in the target app —
|
|
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 and QuickBooks (DEV-487).
|
|
33
|
+
*/
|
|
34
|
+
lookup?: TransformLookup;
|
|
35
|
+
}
|
|
36
|
+
/** A record in the target app, found by one of its fields (DEV-480). */
|
|
37
|
+
export interface TransformLookup {
|
|
38
|
+
/** The target app's object (Odoo model): res.partner, product.product. */
|
|
39
|
+
object: string;
|
|
40
|
+
/** The field the mapped value is matched against: email, default_code. Must be unique. */
|
|
41
|
+
matchField: string;
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* One record per key (DEV-478): rows whose `key` resolves to the same value
|
|
@@ -33,10 +46,26 @@ export interface FieldMapping {
|
|
|
33
46
|
* `lineField` become a list of line objects under `into`, in row order.
|
|
34
47
|
*/
|
|
35
48
|
export interface TransformGroupBy {
|
|
36
|
-
/** Source expression of the key, written like a sourceField: `{{orders[].number}}`. */
|
|
37
|
-
key
|
|
49
|
+
/** Source expression of the key, written like a sourceField: `{{orders[].number}}`. Optional with `linesFrom`. */
|
|
50
|
+
key?: string;
|
|
38
51
|
/** Target field holding the list of lines: `line_items` (Shopify), `Line` (QuickBooks), `order_line` (Odoo). */
|
|
39
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;
|
|
40
69
|
}
|
|
41
70
|
export type TransformMode = 'visual' | 'expression';
|
|
42
71
|
/**
|
|
@@ -78,8 +107,10 @@ export interface TransformConfigType {
|
|
|
78
107
|
/** Explicit upsert/update match field (legacy externalid/pkey). Empty =
|
|
79
108
|
* per-connector auto-detection (Mongo _id, SF Id, SQL primary key). */
|
|
80
109
|
upsertMatchKey?: string;
|
|
81
|
-
/** 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). */
|
|
82
111
|
groupBy?: TransformGroupBy;
|
|
112
|
+
/** One row per element of a list on each row, before mapping (DEV-484). */
|
|
113
|
+
unwind?: TransformUnwind;
|
|
83
114
|
/** Ordered column header names — the exact columns + header text written. */
|
|
84
115
|
sheetHeaders?: string[];
|
|
85
116
|
/** Create the target tab on first write if it doesn't exist (default true). */
|
|
@@ -139,6 +170,11 @@ export declare const TransformConfig: {
|
|
|
139
170
|
required: boolean;
|
|
140
171
|
description: string;
|
|
141
172
|
};
|
|
173
|
+
unwind: {
|
|
174
|
+
type: string;
|
|
175
|
+
required: boolean;
|
|
176
|
+
description: string;
|
|
177
|
+
};
|
|
142
178
|
expression: {
|
|
143
179
|
type: string;
|
|
144
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;
|
package/dist/index.cjs
CHANGED
|
@@ -3340,7 +3340,13 @@ var ForEachConfig = {
|
|
|
3340
3340
|
itemVariable: { type: "string", required: true },
|
|
3341
3341
|
indexVariable: { type: "string", required: false },
|
|
3342
3342
|
batchSize: { type: "number", required: false },
|
|
3343
|
-
continueOnError: { type: "boolean", required: false }
|
|
3343
|
+
continueOnError: { type: "boolean", required: false },
|
|
3344
|
+
collect: {
|
|
3345
|
+
type: "select",
|
|
3346
|
+
required: false,
|
|
3347
|
+
options: ["array", "none", "groupBy", "reduce", "rows"],
|
|
3348
|
+
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."
|
|
3349
|
+
}
|
|
3344
3350
|
},
|
|
3345
3351
|
validate: (config) => {
|
|
3346
3352
|
const errors = [];
|
|
@@ -3725,6 +3731,34 @@ var TRANSFORM_OPERATIONS = [
|
|
|
3725
3731
|
];
|
|
3726
3732
|
var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
|
|
3727
3733
|
var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "google_sheets"]);
|
|
3734
|
+
function groupByErrors(config) {
|
|
3735
|
+
const groupBy = config.groupBy;
|
|
3736
|
+
if (!groupBy || !(groupBy.key || groupBy.into || groupBy.linesFrom))
|
|
3737
|
+
return [];
|
|
3738
|
+
const errors = [];
|
|
3739
|
+
if (!groupBy.key && !groupBy.linesFrom) {
|
|
3740
|
+
errors.push("One record per key: choose the key field (groupBy.key)");
|
|
3741
|
+
}
|
|
3742
|
+
if (!groupBy.into)
|
|
3743
|
+
errors.push("One record per key: name the field that holds the lines (groupBy.into)");
|
|
3744
|
+
if ((groupBy.key || groupBy.linesFrom) && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
|
|
3745
|
+
errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
|
|
3746
|
+
}
|
|
3747
|
+
return errors;
|
|
3748
|
+
}
|
|
3749
|
+
function unwindErrors(config) {
|
|
3750
|
+
const unwind = config.unwind;
|
|
3751
|
+
if (!unwind)
|
|
3752
|
+
return [];
|
|
3753
|
+
if (!unwind.path?.trim()) {
|
|
3754
|
+
return unwind.keepEmpty !== undefined ? ["One row per item: choose the list (unwind.path)"] : [];
|
|
3755
|
+
}
|
|
3756
|
+
const linesFrom = config.groupBy?.linesFrom;
|
|
3757
|
+
if (linesFrom && linesFrom.trim() === unwind.path.trim()) {
|
|
3758
|
+
return ["One row per item and Lines come from name the same list — use one of them (unwind.path, groupBy.linesFrom)"];
|
|
3759
|
+
}
|
|
3760
|
+
return [];
|
|
3761
|
+
}
|
|
3728
3762
|
var TransformConfig = {
|
|
3729
3763
|
defaults: {
|
|
3730
3764
|
name: "Transform",
|
|
@@ -3774,12 +3808,17 @@ var TransformConfig = {
|
|
|
3774
3808
|
mappings: {
|
|
3775
3809
|
type: "array",
|
|
3776
3810
|
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."
|
|
3811
|
+
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
3812
|
},
|
|
3779
3813
|
groupBy: {
|
|
3780
3814
|
type: "json",
|
|
3781
3815
|
required: false,
|
|
3782
|
-
description:
|
|
3816
|
+
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)."
|
|
3817
|
+
},
|
|
3818
|
+
unwind: {
|
|
3819
|
+
type: "json",
|
|
3820
|
+
required: false,
|
|
3821
|
+
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
3822
|
},
|
|
3784
3823
|
expression: { type: "code", required: false }
|
|
3785
3824
|
},
|
|
@@ -3818,19 +3857,14 @@ var TransformConfig = {
|
|
|
3818
3857
|
if (!mapping.targetField) {
|
|
3819
3858
|
errors.push(`Mapping ${index + 1}: Target field is required`);
|
|
3820
3859
|
}
|
|
3860
|
+
const lookup = mapping.lookup;
|
|
3861
|
+
if (lookup && (!lookup.object || !lookup.matchField)) {
|
|
3862
|
+
errors.push(`Mapping ${index + 1}: Find by needs both the object and the field to match (lookup.object, lookup.matchField)`);
|
|
3863
|
+
}
|
|
3821
3864
|
});
|
|
3822
3865
|
}
|
|
3823
3866
|
}
|
|
3824
|
-
|
|
3825
|
-
if (groupBy && (groupBy.key || groupBy.into)) {
|
|
3826
|
-
if (!groupBy.key)
|
|
3827
|
-
errors.push("One record per key: choose the key field (groupBy.key)");
|
|
3828
|
-
if (!groupBy.into)
|
|
3829
|
-
errors.push("One record per key: name the field that holds the lines (groupBy.into)");
|
|
3830
|
-
if (groupBy.key && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
|
|
3831
|
-
errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3867
|
+
errors.push(...groupByErrors(config), ...unwindErrors(config));
|
|
3834
3868
|
return { valid: errors.length === 0, errors };
|
|
3835
3869
|
},
|
|
3836
3870
|
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, } 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 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";
|
package/dist/index.js
CHANGED
|
@@ -3182,7 +3182,13 @@ var ForEachConfig = {
|
|
|
3182
3182
|
itemVariable: { type: "string", required: true },
|
|
3183
3183
|
indexVariable: { type: "string", required: false },
|
|
3184
3184
|
batchSize: { type: "number", required: false },
|
|
3185
|
-
continueOnError: { type: "boolean", required: false }
|
|
3185
|
+
continueOnError: { type: "boolean", required: false },
|
|
3186
|
+
collect: {
|
|
3187
|
+
type: "select",
|
|
3188
|
+
required: false,
|
|
3189
|
+
options: ["array", "none", "groupBy", "reduce", "rows"],
|
|
3190
|
+
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."
|
|
3191
|
+
}
|
|
3186
3192
|
},
|
|
3187
3193
|
validate: (config) => {
|
|
3188
3194
|
const errors = [];
|
|
@@ -3567,6 +3573,34 @@ var TRANSFORM_OPERATIONS = [
|
|
|
3567
3573
|
];
|
|
3568
3574
|
var TRANSFORM_OPERATION_IDS = TRANSFORM_OPERATIONS.map((o) => o.id);
|
|
3569
3575
|
var DATABASE_CONNECTOR_IDS = new Set(["mongodb", "postgresql", "mysql", "google_sheets"]);
|
|
3576
|
+
function groupByErrors(config) {
|
|
3577
|
+
const groupBy = config.groupBy;
|
|
3578
|
+
if (!groupBy || !(groupBy.key || groupBy.into || groupBy.linesFrom))
|
|
3579
|
+
return [];
|
|
3580
|
+
const errors = [];
|
|
3581
|
+
if (!groupBy.key && !groupBy.linesFrom) {
|
|
3582
|
+
errors.push("One record per key: choose the key field (groupBy.key)");
|
|
3583
|
+
}
|
|
3584
|
+
if (!groupBy.into)
|
|
3585
|
+
errors.push("One record per key: name the field that holds the lines (groupBy.into)");
|
|
3586
|
+
if ((groupBy.key || groupBy.linesFrom) && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
|
|
3587
|
+
errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
|
|
3588
|
+
}
|
|
3589
|
+
return errors;
|
|
3590
|
+
}
|
|
3591
|
+
function unwindErrors(config) {
|
|
3592
|
+
const unwind = config.unwind;
|
|
3593
|
+
if (!unwind)
|
|
3594
|
+
return [];
|
|
3595
|
+
if (!unwind.path?.trim()) {
|
|
3596
|
+
return unwind.keepEmpty !== undefined ? ["One row per item: choose the list (unwind.path)"] : [];
|
|
3597
|
+
}
|
|
3598
|
+
const linesFrom = config.groupBy?.linesFrom;
|
|
3599
|
+
if (linesFrom && linesFrom.trim() === unwind.path.trim()) {
|
|
3600
|
+
return ["One row per item and Lines come from name the same list — use one of them (unwind.path, groupBy.linesFrom)"];
|
|
3601
|
+
}
|
|
3602
|
+
return [];
|
|
3603
|
+
}
|
|
3570
3604
|
var TransformConfig = {
|
|
3571
3605
|
defaults: {
|
|
3572
3606
|
name: "Transform",
|
|
@@ -3616,12 +3650,17 @@ var TransformConfig = {
|
|
|
3616
3650
|
mappings: {
|
|
3617
3651
|
type: "array",
|
|
3618
3652
|
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."
|
|
3653
|
+
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
3654
|
},
|
|
3621
3655
|
groupBy: {
|
|
3622
3656
|
type: "json",
|
|
3623
3657
|
required: false,
|
|
3624
|
-
description:
|
|
3658
|
+
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)."
|
|
3659
|
+
},
|
|
3660
|
+
unwind: {
|
|
3661
|
+
type: "json",
|
|
3662
|
+
required: false,
|
|
3663
|
+
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
3664
|
},
|
|
3626
3665
|
expression: { type: "code", required: false }
|
|
3627
3666
|
},
|
|
@@ -3660,19 +3699,14 @@ var TransformConfig = {
|
|
|
3660
3699
|
if (!mapping.targetField) {
|
|
3661
3700
|
errors.push(`Mapping ${index + 1}: Target field is required`);
|
|
3662
3701
|
}
|
|
3702
|
+
const lookup = mapping.lookup;
|
|
3703
|
+
if (lookup && (!lookup.object || !lookup.matchField)) {
|
|
3704
|
+
errors.push(`Mapping ${index + 1}: Find by needs both the object and the field to match (lookup.object, lookup.matchField)`);
|
|
3705
|
+
}
|
|
3663
3706
|
});
|
|
3664
3707
|
}
|
|
3665
3708
|
}
|
|
3666
|
-
|
|
3667
|
-
if (groupBy && (groupBy.key || groupBy.into)) {
|
|
3668
|
-
if (!groupBy.key)
|
|
3669
|
-
errors.push("One record per key: choose the key field (groupBy.key)");
|
|
3670
|
-
if (!groupBy.into)
|
|
3671
|
-
errors.push("One record per key: name the field that holds the lines (groupBy.into)");
|
|
3672
|
-
if (groupBy.key && groupBy.into && !(config.mappings ?? []).some((m) => m.lineField)) {
|
|
3673
|
-
errors.push("One record per key: mark at least one mapping as a line field (lineField: true)");
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3709
|
+
errors.push(...groupByErrors(config), ...unwindErrors(config));
|
|
3676
3710
|
return { valid: errors.length === 0, errors };
|
|
3677
3711
|
},
|
|
3678
3712
|
merge: (userConfig) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octaviaflow/flow-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|