@cyoda/workflow-core 0.1.0 → 0.2.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/README.md +93 -7
- package/dist/index.cjs +952 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +298 -87
- package/dist/index.d.ts +298 -87
- package/dist/index.js +938 -113
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ArrayCriterionSchema: () => ArrayCriterionSchema,
|
|
24
|
+
CRITERION_DEPTH_WARNING_THRESHOLD: () => CRITERION_DEPTH_WARNING_THRESHOLD,
|
|
24
25
|
CriterionSchema: () => CriterionSchema,
|
|
25
26
|
ExecutionModeSchema: () => ExecutionModeSchema,
|
|
26
27
|
ExportPayloadSchema: () => ExportPayloadSchema,
|
|
@@ -30,26 +31,38 @@ __export(index_exports, {
|
|
|
30
31
|
GroupCriterionSchema: () => GroupCriterionSchema,
|
|
31
32
|
ImportPayloadSchema: () => ImportPayloadSchema,
|
|
32
33
|
LifecycleCriterionSchema: () => LifecycleCriterionSchema,
|
|
34
|
+
MAX_CRITERION_DEPTH: () => MAX_CRITERION_DEPTH,
|
|
35
|
+
MAX_JSON_BYTES: () => MAX_JSON_BYTES,
|
|
36
|
+
MAX_JSON_OBJECT_DEPTH: () => MAX_JSON_OBJECT_DEPTH,
|
|
33
37
|
NAME_REGEX: () => NAME_REGEX,
|
|
34
38
|
NameSchema: () => NameSchema,
|
|
39
|
+
OPERATOR_GROUPS: () => OPERATOR_GROUPS,
|
|
35
40
|
OPERATOR_TYPES: () => OPERATOR_TYPES,
|
|
41
|
+
OPERATOR_VALUE_SHAPE: () => OPERATOR_VALUE_SHAPE,
|
|
36
42
|
OperatorEnum: () => OperatorEnum,
|
|
37
43
|
ParseJsonError: () => ParseJsonError,
|
|
44
|
+
PatchConflictError: () => PatchConflictError,
|
|
38
45
|
ProcessorSchema: () => ProcessorSchema,
|
|
46
|
+
SUPPORTED_GROUP_OPERATORS: () => SUPPORTED_GROUP_OPERATORS,
|
|
47
|
+
SUPPORTED_SIMPLE_OPERATORS: () => SUPPORTED_SIMPLE_OPERATORS,
|
|
39
48
|
ScheduledProcessorSchema: () => ScheduledProcessorSchema,
|
|
40
49
|
SchemaError: () => SchemaError,
|
|
41
50
|
SimpleCriterionSchema: () => SimpleCriterionSchema,
|
|
42
51
|
StateSchema: () => StateSchema,
|
|
43
52
|
TransitionSchema: () => TransitionSchema,
|
|
53
|
+
UNSUPPORTED_OPERATORS: () => UNSUPPORTED_OPERATORS,
|
|
44
54
|
WorkflowApiConflictError: () => WorkflowApiConflictError,
|
|
45
55
|
WorkflowApiTransportError: () => WorkflowApiTransportError,
|
|
46
56
|
WorkflowSchema: () => WorkflowSchema,
|
|
47
57
|
applyPatch: () => applyPatch,
|
|
48
58
|
applyPatches: () => applyPatches,
|
|
59
|
+
applyTransaction: () => applyTransaction,
|
|
49
60
|
assignSyntheticIds: () => assignSyntheticIds,
|
|
61
|
+
describeCriterion: () => describeCriterion,
|
|
50
62
|
findMigrationPath: () => findMigrationPath,
|
|
51
|
-
idFor: () =>
|
|
63
|
+
idFor: () => idFor,
|
|
52
64
|
invertPatch: () => invertPatch,
|
|
65
|
+
invertTransaction: () => invertTransaction,
|
|
53
66
|
listMigrations: () => listMigrations,
|
|
54
67
|
lookupById: () => lookupById,
|
|
55
68
|
migrateSession: () => migrateSession,
|
|
@@ -75,6 +88,7 @@ __export(index_exports, {
|
|
|
75
88
|
validateAll: () => validateAll,
|
|
76
89
|
validateExportSchema: () => validateExportSchema,
|
|
77
90
|
validateImportSchema: () => validateImportSchema,
|
|
91
|
+
validateJsonPathSubset: () => validateJsonPathSubset,
|
|
78
92
|
validateSemantics: () => validateSemantics,
|
|
79
93
|
validateSession: () => validateSession,
|
|
80
94
|
zodErrorToIssues: () => zodErrorToIssues
|
|
@@ -113,6 +127,14 @@ var OPERATOR_TYPES = /* @__PURE__ */ new Set([
|
|
|
113
127
|
"IS_CHANGED"
|
|
114
128
|
]);
|
|
115
129
|
|
|
130
|
+
// src/types/transaction.ts
|
|
131
|
+
var PatchConflictError = class extends Error {
|
|
132
|
+
constructor(message) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "PatchConflictError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
116
138
|
// src/types/api.ts
|
|
117
139
|
var WorkflowApiConflictError = class extends Error {
|
|
118
140
|
constructor(entity, serverConcurrencyToken, message = "Workflow save conflict: server state has changed.") {
|
|
@@ -227,11 +249,17 @@ var FunctionCriterionSchema = import_zod3.z.lazy(
|
|
|
227
249
|
|
|
228
250
|
// src/schema/processor.ts
|
|
229
251
|
var import_zod4 = require("zod");
|
|
230
|
-
var ExecutionModeSchema = import_zod4.z.enum([
|
|
252
|
+
var ExecutionModeSchema = import_zod4.z.enum([
|
|
253
|
+
"SYNC",
|
|
254
|
+
"ASYNC_SAME_TX",
|
|
255
|
+
"ASYNC_NEW_TX",
|
|
256
|
+
"COMMIT_BEFORE_DISPATCH"
|
|
257
|
+
]);
|
|
231
258
|
var ExternalizedProcessorSchema = import_zod4.z.object({
|
|
232
259
|
type: import_zod4.z.literal("externalized"),
|
|
233
260
|
name: NameSchema,
|
|
234
261
|
executionMode: ExecutionModeSchema.optional(),
|
|
262
|
+
startNewTxOnDispatch: import_zod4.z.boolean().optional(),
|
|
235
263
|
config: FunctionConfigSchema.and(
|
|
236
264
|
import_zod4.z.object({
|
|
237
265
|
asyncResult: import_zod4.z.boolean().optional(),
|
|
@@ -259,7 +287,7 @@ var TransitionSchema = import_zod5.z.object({
|
|
|
259
287
|
name: NameSchema,
|
|
260
288
|
next: NameSchema,
|
|
261
289
|
manual: import_zod5.z.boolean(),
|
|
262
|
-
disabled: import_zod5.z.boolean(),
|
|
290
|
+
disabled: import_zod5.z.boolean().default(false),
|
|
263
291
|
criterion: CriterionSchema.optional(),
|
|
264
292
|
processors: import_zod5.z.array(ProcessorSchema).optional()
|
|
265
293
|
});
|
|
@@ -472,6 +500,118 @@ function mintCriterionIds(c, host, path, ids) {
|
|
|
472
500
|
}
|
|
473
501
|
}
|
|
474
502
|
|
|
503
|
+
// src/criteria/operators.ts
|
|
504
|
+
var SUPPORTED_SIMPLE_OPERATORS = /* @__PURE__ */ new Set([
|
|
505
|
+
"EQUALS",
|
|
506
|
+
"NOT_EQUAL",
|
|
507
|
+
"GREATER_THAN",
|
|
508
|
+
"LESS_THAN",
|
|
509
|
+
"GREATER_OR_EQUAL",
|
|
510
|
+
"LESS_OR_EQUAL",
|
|
511
|
+
"CONTAINS",
|
|
512
|
+
"NOT_CONTAINS",
|
|
513
|
+
"STARTS_WITH",
|
|
514
|
+
"NOT_STARTS_WITH",
|
|
515
|
+
"ENDS_WITH",
|
|
516
|
+
"NOT_ENDS_WITH",
|
|
517
|
+
"LIKE",
|
|
518
|
+
"IS_NULL",
|
|
519
|
+
"NOT_NULL",
|
|
520
|
+
"BETWEEN",
|
|
521
|
+
"BETWEEN_INCLUSIVE",
|
|
522
|
+
"MATCHES_PATTERN",
|
|
523
|
+
"IEQUALS",
|
|
524
|
+
"INOT_EQUAL",
|
|
525
|
+
"ICONTAINS",
|
|
526
|
+
"INOT_CONTAINS",
|
|
527
|
+
"ISTARTS_WITH",
|
|
528
|
+
"INOT_STARTS_WITH",
|
|
529
|
+
"IENDS_WITH",
|
|
530
|
+
"INOT_ENDS_WITH"
|
|
531
|
+
]);
|
|
532
|
+
var UNSUPPORTED_OPERATORS = /* @__PURE__ */ new Set([
|
|
533
|
+
"IS_UNCHANGED",
|
|
534
|
+
"IS_CHANGED"
|
|
535
|
+
]);
|
|
536
|
+
var SUPPORTED_GROUP_OPERATORS = ["AND", "OR"];
|
|
537
|
+
var MAX_CRITERION_DEPTH = 50;
|
|
538
|
+
var CRITERION_DEPTH_WARNING_THRESHOLD = 5;
|
|
539
|
+
var OPERATOR_GROUPS = [
|
|
540
|
+
{
|
|
541
|
+
id: "equality",
|
|
542
|
+
label: "Equality",
|
|
543
|
+
operators: ["EQUALS", "NOT_EQUAL", "IEQUALS", "INOT_EQUAL"]
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "ordering",
|
|
547
|
+
label: "Ordering",
|
|
548
|
+
operators: ["GREATER_THAN", "LESS_THAN", "GREATER_OR_EQUAL", "LESS_OR_EQUAL"]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: "range",
|
|
552
|
+
label: "Range",
|
|
553
|
+
operators: ["BETWEEN", "BETWEEN_INCLUSIVE"]
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
id: "substring",
|
|
557
|
+
label: "Substring",
|
|
558
|
+
operators: [
|
|
559
|
+
"CONTAINS",
|
|
560
|
+
"NOT_CONTAINS",
|
|
561
|
+
"ICONTAINS",
|
|
562
|
+
"INOT_CONTAINS",
|
|
563
|
+
"STARTS_WITH",
|
|
564
|
+
"NOT_STARTS_WITH",
|
|
565
|
+
"ISTARTS_WITH",
|
|
566
|
+
"INOT_STARTS_WITH",
|
|
567
|
+
"ENDS_WITH",
|
|
568
|
+
"NOT_ENDS_WITH",
|
|
569
|
+
"IENDS_WITH",
|
|
570
|
+
"INOT_ENDS_WITH"
|
|
571
|
+
]
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: "pattern",
|
|
575
|
+
label: "Pattern",
|
|
576
|
+
operators: ["LIKE", "MATCHES_PATTERN"]
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "null",
|
|
580
|
+
label: "Null",
|
|
581
|
+
operators: ["IS_NULL", "NOT_NULL"]
|
|
582
|
+
}
|
|
583
|
+
];
|
|
584
|
+
var OPERATOR_VALUE_SHAPE = {
|
|
585
|
+
EQUALS: "scalar",
|
|
586
|
+
NOT_EQUAL: "scalar",
|
|
587
|
+
GREATER_THAN: "scalar",
|
|
588
|
+
LESS_THAN: "scalar",
|
|
589
|
+
GREATER_OR_EQUAL: "scalar",
|
|
590
|
+
LESS_OR_EQUAL: "scalar",
|
|
591
|
+
CONTAINS: "scalar",
|
|
592
|
+
NOT_CONTAINS: "scalar",
|
|
593
|
+
STARTS_WITH: "scalar",
|
|
594
|
+
NOT_STARTS_WITH: "scalar",
|
|
595
|
+
ENDS_WITH: "scalar",
|
|
596
|
+
NOT_ENDS_WITH: "scalar",
|
|
597
|
+
LIKE: "scalar",
|
|
598
|
+
MATCHES_PATTERN: "scalar",
|
|
599
|
+
IEQUALS: "scalar",
|
|
600
|
+
INOT_EQUAL: "scalar",
|
|
601
|
+
ICONTAINS: "scalar",
|
|
602
|
+
INOT_CONTAINS: "scalar",
|
|
603
|
+
ISTARTS_WITH: "scalar",
|
|
604
|
+
INOT_STARTS_WITH: "scalar",
|
|
605
|
+
IENDS_WITH: "scalar",
|
|
606
|
+
INOT_ENDS_WITH: "scalar",
|
|
607
|
+
BETWEEN: "range",
|
|
608
|
+
BETWEEN_INCLUSIVE: "range",
|
|
609
|
+
IS_NULL: "none",
|
|
610
|
+
NOT_NULL: "none",
|
|
611
|
+
IS_UNCHANGED: "none",
|
|
612
|
+
IS_CHANGED: "none"
|
|
613
|
+
};
|
|
614
|
+
|
|
475
615
|
// src/normalize/input.ts
|
|
476
616
|
function normalizeWorkflowInput(workflow) {
|
|
477
617
|
const out = {
|
|
@@ -514,12 +654,21 @@ function normalizeWorkflowInput(workflow) {
|
|
|
514
654
|
}
|
|
515
655
|
return out;
|
|
516
656
|
}
|
|
517
|
-
function normalizeCriterion(criterion) {
|
|
657
|
+
function normalizeCriterion(criterion, depth = 0) {
|
|
658
|
+
if (depth >= MAX_CRITERION_DEPTH) {
|
|
659
|
+
return criterion;
|
|
660
|
+
}
|
|
518
661
|
switch (criterion.type) {
|
|
519
662
|
case "simple":
|
|
663
|
+
if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
|
|
664
|
+
return { ...criterion, value: null };
|
|
665
|
+
}
|
|
520
666
|
return criterion;
|
|
521
667
|
case "group":
|
|
522
|
-
return {
|
|
668
|
+
return {
|
|
669
|
+
...criterion,
|
|
670
|
+
conditions: criterion.conditions.map((c) => normalizeCriterion(c, depth + 1))
|
|
671
|
+
};
|
|
523
672
|
case "function": {
|
|
524
673
|
const fn = criterion.function;
|
|
525
674
|
const out = {
|
|
@@ -527,12 +676,15 @@ function normalizeCriterion(criterion) {
|
|
|
527
676
|
function: {
|
|
528
677
|
name: fn.name.trim(),
|
|
529
678
|
...fn.config !== void 0 ? { config: fn.config } : {},
|
|
530
|
-
...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion) } : {}
|
|
679
|
+
...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion, depth + 1) } : {}
|
|
531
680
|
}
|
|
532
681
|
};
|
|
533
682
|
return out;
|
|
534
683
|
}
|
|
535
684
|
case "lifecycle":
|
|
685
|
+
if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
|
|
686
|
+
return { ...criterion, value: null };
|
|
687
|
+
}
|
|
536
688
|
return criterion;
|
|
537
689
|
case "array":
|
|
538
690
|
return criterion;
|
|
@@ -549,6 +701,79 @@ function normalizeProcessor(p) {
|
|
|
549
701
|
};
|
|
550
702
|
}
|
|
551
703
|
|
|
704
|
+
// src/criteria/jsonPathSubset.ts
|
|
705
|
+
var SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
706
|
+
var INDEX_RE = /^(?:\d+|\*)$/;
|
|
707
|
+
function validateJsonPathSubset(path) {
|
|
708
|
+
if (path.length === 0) return { ok: false, reason: "empty" };
|
|
709
|
+
if (path[0] !== "$") return { ok: false, reason: "missing-root" };
|
|
710
|
+
if (path === "$") return { ok: true };
|
|
711
|
+
let i = 1;
|
|
712
|
+
while (i < path.length) {
|
|
713
|
+
const ch = path[i];
|
|
714
|
+
if (ch === ".") {
|
|
715
|
+
if (path[i + 1] === ".") return { ok: false, reason: "recursive-descent" };
|
|
716
|
+
i += 1;
|
|
717
|
+
const start = i;
|
|
718
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[") i += 1;
|
|
719
|
+
const segment = path.slice(start, i);
|
|
720
|
+
if (!SEGMENT_RE.test(segment)) return { ok: false, reason: "malformed" };
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (ch === "[") {
|
|
724
|
+
if (path[i + 1] === "?") return { ok: false, reason: "filter-expression" };
|
|
725
|
+
if (path[i + 1] === "'" || path[i + 1] === '"') return { ok: false, reason: "malformed" };
|
|
726
|
+
const end = path.indexOf("]", i);
|
|
727
|
+
if (end === -1) return { ok: false, reason: "malformed" };
|
|
728
|
+
const inner = path.slice(i + 1, end);
|
|
729
|
+
if (!INDEX_RE.test(inner)) return { ok: false, reason: "malformed" };
|
|
730
|
+
i = end + 1;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
return { ok: false, reason: "malformed" };
|
|
734
|
+
}
|
|
735
|
+
return { ok: true };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/identity/id-for.ts
|
|
739
|
+
function idFor(meta, ref) {
|
|
740
|
+
switch (ref.kind) {
|
|
741
|
+
case "workflow":
|
|
742
|
+
return meta.ids.workflows[ref.workflow] ?? null;
|
|
743
|
+
case "state": {
|
|
744
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
|
|
745
|
+
if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
case "transition": {
|
|
750
|
+
const matches = [];
|
|
751
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
752
|
+
if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
|
|
753
|
+
matches.push(uuid);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return matches[ref.ordinal] ?? null;
|
|
757
|
+
}
|
|
758
|
+
case "processor": {
|
|
759
|
+
const matches = [];
|
|
760
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
|
|
761
|
+
if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
|
|
762
|
+
}
|
|
763
|
+
return matches[ref.ordinal] ?? null;
|
|
764
|
+
}
|
|
765
|
+
case "criterion": {
|
|
766
|
+
const target = JSON.stringify(ref.path);
|
|
767
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
|
|
768
|
+
if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
|
|
769
|
+
return uuid;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
552
777
|
// src/validate/helpers.ts
|
|
553
778
|
function isValidName(name) {
|
|
554
779
|
return NAME_REGEX.test(name);
|
|
@@ -574,12 +799,15 @@ function* walkCriteria(session) {
|
|
|
574
799
|
}
|
|
575
800
|
}
|
|
576
801
|
}
|
|
577
|
-
function* walkInner(c, where) {
|
|
802
|
+
function* walkInner(c, where, depth = 0) {
|
|
578
803
|
yield { criterion: c, where };
|
|
804
|
+
if (depth >= MAX_CRITERION_DEPTH) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
579
807
|
if (c.type === "group") {
|
|
580
|
-
for (const child of c.conditions) yield* walkInner(child, where);
|
|
808
|
+
for (const child of c.conditions) yield* walkInner(child, where, depth + 1);
|
|
581
809
|
} else if (c.type === "function" && c.function.criterion) {
|
|
582
|
-
yield* walkInner(c.function.criterion, where);
|
|
810
|
+
yield* walkInner(c.function.criterion, where, depth + 1);
|
|
583
811
|
}
|
|
584
812
|
}
|
|
585
813
|
|
|
@@ -592,6 +820,8 @@ function validateSemantics(session, doc) {
|
|
|
592
820
|
issues.push(...validateWorkflow(wf, doc));
|
|
593
821
|
}
|
|
594
822
|
issues.push(...criterionRules(session));
|
|
823
|
+
issues.push(...criterionDepthRules(session));
|
|
824
|
+
issues.push(...automatedOrderingRules(session, doc));
|
|
595
825
|
if (session.workflows.length === 1) {
|
|
596
826
|
const only = session.workflows[0];
|
|
597
827
|
if (only && only.criterion !== void 0) {
|
|
@@ -629,14 +859,14 @@ function validateWorkflow(wf, doc) {
|
|
|
629
859
|
severity: "error",
|
|
630
860
|
code: "missing-initial-state",
|
|
631
861
|
message: `Workflow "${wf.name}" has no initialState.`,
|
|
632
|
-
...
|
|
862
|
+
...idFor2(doc, wf.name, "workflow")
|
|
633
863
|
});
|
|
634
864
|
} else if (!(wf.initialState in wf.states)) {
|
|
635
865
|
issues.push({
|
|
636
866
|
severity: "error",
|
|
637
867
|
code: "unknown-initial-state",
|
|
638
868
|
message: `Workflow "${wf.name}" initialState "${wf.initialState}" is not a state.`,
|
|
639
|
-
...
|
|
869
|
+
...idFor2(doc, wf.name, "workflow")
|
|
640
870
|
});
|
|
641
871
|
}
|
|
642
872
|
if (!isValidName(wf.name)) {
|
|
@@ -689,6 +919,13 @@ function validateWorkflow(wf, doc) {
|
|
|
689
919
|
message: `Scheduled processor "${p.name}" has empty target transition.`
|
|
690
920
|
});
|
|
691
921
|
}
|
|
922
|
+
if (p.type === "externalized" && p.startNewTxOnDispatch === true && p.executionMode !== "COMMIT_BEFORE_DISPATCH") {
|
|
923
|
+
issues.push({
|
|
924
|
+
severity: "warning",
|
|
925
|
+
code: "start-new-tx-without-commit-before-dispatch",
|
|
926
|
+
message: `Processor "${p.name}" sets startNewTxOnDispatch but executionMode is not COMMIT_BEFORE_DISPATCH.`
|
|
927
|
+
});
|
|
928
|
+
}
|
|
692
929
|
if (p.type === "externalized" && p.config) {
|
|
693
930
|
if (p.config.crossoverToAsyncMs !== void 0 && p.config.asyncResult !== true) {
|
|
694
931
|
issues.push({
|
|
@@ -831,7 +1068,16 @@ function criterionRules(session) {
|
|
|
831
1068
|
});
|
|
832
1069
|
}
|
|
833
1070
|
break;
|
|
834
|
-
case "array":
|
|
1071
|
+
case "array": {
|
|
1072
|
+
const arrPathCheck = validateJsonPathSubset(criterion.jsonPath);
|
|
1073
|
+
if (!arrPathCheck.ok) {
|
|
1074
|
+
issues.push({
|
|
1075
|
+
severity: "error",
|
|
1076
|
+
code: "invalid-jsonpath-subset",
|
|
1077
|
+
message: `Array criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${arrPathCheck.reason}) (at ${describe(where)}).`,
|
|
1078
|
+
detail: { jsonPath: criterion.jsonPath, reason: arrPathCheck.reason }
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
835
1081
|
for (const v of criterion.value) {
|
|
836
1082
|
if (typeof v !== "string") {
|
|
837
1083
|
issues.push({
|
|
@@ -843,6 +1089,7 @@ function criterionRules(session) {
|
|
|
843
1089
|
}
|
|
844
1090
|
}
|
|
845
1091
|
break;
|
|
1092
|
+
}
|
|
846
1093
|
case "lifecycle":
|
|
847
1094
|
if (!LIFECYCLE_FIELDS.has(criterion.field)) {
|
|
848
1095
|
issues.push({
|
|
@@ -851,22 +1098,144 @@ function criterionRules(session) {
|
|
|
851
1098
|
message: `Lifecycle criterion field "${criterion.field}" is invalid.`
|
|
852
1099
|
});
|
|
853
1100
|
}
|
|
1101
|
+
if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
|
|
1102
|
+
issues.push({
|
|
1103
|
+
severity: "warning",
|
|
1104
|
+
code: "unsupported-operator",
|
|
1105
|
+
message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
|
|
1106
|
+
detail: { operation: criterion.operation }
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
854
1109
|
break;
|
|
855
1110
|
case "group":
|
|
856
|
-
if (criterion.operator === "NOT"
|
|
1111
|
+
if (criterion.operator === "NOT") {
|
|
857
1112
|
issues.push({
|
|
858
1113
|
severity: "warning",
|
|
859
|
-
code: "
|
|
860
|
-
message: `NOT
|
|
1114
|
+
code: "unsupported-group-operator",
|
|
1115
|
+
message: `Group operator "NOT" is not implemented by the engine (at ${describe(where)}).`,
|
|
1116
|
+
detail: { operator: "NOT" }
|
|
861
1117
|
});
|
|
1118
|
+
if (criterion.conditions.length > 1) {
|
|
1119
|
+
issues.push({
|
|
1120
|
+
severity: "warning",
|
|
1121
|
+
code: "not-with-multiple-conditions",
|
|
1122
|
+
message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
862
1125
|
}
|
|
863
1126
|
break;
|
|
864
|
-
case "simple":
|
|
1127
|
+
case "simple": {
|
|
1128
|
+
const pathCheck = validateJsonPathSubset(criterion.jsonPath);
|
|
1129
|
+
if (!pathCheck.ok) {
|
|
1130
|
+
issues.push({
|
|
1131
|
+
severity: "error",
|
|
1132
|
+
code: "invalid-jsonpath-subset",
|
|
1133
|
+
message: `Simple criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${pathCheck.reason}) (at ${describe(where)}).`,
|
|
1134
|
+
detail: { jsonPath: criterion.jsonPath, reason: pathCheck.reason }
|
|
1135
|
+
});
|
|
1136
|
+
} else if (criterion.jsonPath.startsWith("$._meta")) {
|
|
1137
|
+
issues.push({
|
|
1138
|
+
severity: "warning",
|
|
1139
|
+
code: "lifecycle-path-in-simple",
|
|
1140
|
+
message: `Simple criterion path "${criterion.jsonPath}" looks like a lifecycle path; use a lifecycle criterion instead (at ${describe(where)}).`,
|
|
1141
|
+
detail: { jsonPath: criterion.jsonPath }
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
|
|
1145
|
+
issues.push({
|
|
1146
|
+
severity: "warning",
|
|
1147
|
+
code: "unsupported-operator",
|
|
1148
|
+
message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
|
|
1149
|
+
detail: { operation: criterion.operation }
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
if (criterion.operation === "BETWEEN" || criterion.operation === "BETWEEN_INCLUSIVE") {
|
|
1153
|
+
if (!Array.isArray(criterion.value) || criterion.value.length !== 2) {
|
|
1154
|
+
issues.push({
|
|
1155
|
+
severity: "error",
|
|
1156
|
+
code: "simple-between-shape",
|
|
1157
|
+
message: `Operator "${criterion.operation}" requires a two-element [low, high] array value (at ${describe(where)}).`,
|
|
1158
|
+
detail: { operation: criterion.operation }
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (criterion.operation === "LIKE" && typeof criterion.value === "string" && /[%_]/.test(criterion.value)) {
|
|
1163
|
+
issues.push({
|
|
1164
|
+
severity: "warning",
|
|
1165
|
+
code: "like-wildcard-warning",
|
|
1166
|
+
message: `LIKE pattern contains "%" or "_" which are always wildcards (no escape mechanism) (at ${describe(where)}).`
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (criterion.operation === "MATCHES_PATTERN" && typeof criterion.value === "string" && criterion.value.length > 0 && !criterion.value.startsWith("^") && !criterion.value.endsWith("$")) {
|
|
1170
|
+
issues.push({
|
|
1171
|
+
severity: "warning",
|
|
1172
|
+
code: "matches-pattern-unanchored",
|
|
1173
|
+
message: `MATCHES_PATTERN regex is unanchored; include "^"/"$" for whole-string match (at ${describe(where)}).`
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
865
1176
|
break;
|
|
1177
|
+
}
|
|
866
1178
|
}
|
|
867
1179
|
}
|
|
868
1180
|
return issues;
|
|
869
1181
|
}
|
|
1182
|
+
function criterionDepthRules(session) {
|
|
1183
|
+
const issues = [];
|
|
1184
|
+
for (const wf of session.workflows) {
|
|
1185
|
+
if (wf.criterion) {
|
|
1186
|
+
pushDepthIssue(issues, criterionMaxDepth(wf.criterion), {
|
|
1187
|
+
kind: "workflow",
|
|
1188
|
+
workflow: wf.name
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
for (const [stateCode, state] of Object.entries(wf.states)) {
|
|
1192
|
+
for (let i = 0; i < state.transitions.length; i++) {
|
|
1193
|
+
const t = state.transitions[i];
|
|
1194
|
+
if (!t.criterion) continue;
|
|
1195
|
+
pushDepthIssue(issues, criterionMaxDepth(t.criterion), {
|
|
1196
|
+
kind: "transition",
|
|
1197
|
+
workflow: wf.name,
|
|
1198
|
+
state: stateCode,
|
|
1199
|
+
transitionIndex: i,
|
|
1200
|
+
transitionName: t.name
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return issues;
|
|
1206
|
+
}
|
|
1207
|
+
function pushDepthIssue(issues, maxDepth, where) {
|
|
1208
|
+
if (maxDepth >= MAX_CRITERION_DEPTH) {
|
|
1209
|
+
issues.push({
|
|
1210
|
+
severity: "error",
|
|
1211
|
+
code: "criterion-depth-limit",
|
|
1212
|
+
message: `Criterion tree depth ${maxDepth} exceeds engine limit ${MAX_CRITERION_DEPTH} (at ${describe(where)}).`,
|
|
1213
|
+
detail: { maxDepth, threshold: MAX_CRITERION_DEPTH }
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (maxDepth >= CRITERION_DEPTH_WARNING_THRESHOLD) {
|
|
1217
|
+
issues.push({
|
|
1218
|
+
severity: "warning",
|
|
1219
|
+
code: "criterion-depth-warning",
|
|
1220
|
+
message: `Criterion tree depth ${maxDepth} is hard to read; consider flattening (at ${describe(where)}).`,
|
|
1221
|
+
detail: { maxDepth, threshold: CRITERION_DEPTH_WARNING_THRESHOLD }
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
function criterionMaxDepth(root) {
|
|
1226
|
+
const stack = [{ node: root, depth: 1 }];
|
|
1227
|
+
let max = 0;
|
|
1228
|
+
while (stack.length > 0) {
|
|
1229
|
+
const { node, depth } = stack.pop();
|
|
1230
|
+
if (depth > max) max = depth;
|
|
1231
|
+
if (node.type === "group") {
|
|
1232
|
+
for (const child of node.conditions) stack.push({ node: child, depth: depth + 1 });
|
|
1233
|
+
} else if (node.type === "function" && node.function.criterion) {
|
|
1234
|
+
stack.push({ node: node.function.criterion, depth: depth + 1 });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return max;
|
|
1238
|
+
}
|
|
870
1239
|
function reachableStates(wf) {
|
|
871
1240
|
const visited = /* @__PURE__ */ new Set();
|
|
872
1241
|
if (!(wf.initialState in wf.states)) return visited;
|
|
@@ -915,11 +1284,65 @@ function describe(w) {
|
|
|
915
1284
|
if (w.kind === "workflow") return `workflow "${w.workflow}"`;
|
|
916
1285
|
return `transition "${w.transitionName}" on "${w.workflow}:${w.state}"`;
|
|
917
1286
|
}
|
|
918
|
-
function
|
|
1287
|
+
function idFor2(doc, workflowName, _kind) {
|
|
919
1288
|
if (!doc) return {};
|
|
920
1289
|
const id = doc.meta.ids.workflows[workflowName];
|
|
921
1290
|
return id ? { targetId: id } : {};
|
|
922
1291
|
}
|
|
1292
|
+
function transitionTargetId(doc, workflow, state, declarationIndex) {
|
|
1293
|
+
if (!doc) return {};
|
|
1294
|
+
const id = idFor(doc.meta, {
|
|
1295
|
+
kind: "transition",
|
|
1296
|
+
workflow,
|
|
1297
|
+
state,
|
|
1298
|
+
transitionName: "",
|
|
1299
|
+
ordinal: declarationIndex
|
|
1300
|
+
});
|
|
1301
|
+
return id ? { targetId: id } : {};
|
|
1302
|
+
}
|
|
1303
|
+
function automatedOrderingRules(session, doc) {
|
|
1304
|
+
const issues = [];
|
|
1305
|
+
for (const wf of session.workflows) {
|
|
1306
|
+
for (const [stateCode, state] of Object.entries(wf.states)) {
|
|
1307
|
+
const automated = [];
|
|
1308
|
+
state.transitions.forEach((t, index) => {
|
|
1309
|
+
if (t.manual !== true && t.disabled !== true) {
|
|
1310
|
+
automated.push({ index, t });
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
const nullIdx = automated.findIndex(({ t }) => t.criterion === void 0);
|
|
1314
|
+
if (nullIdx === -1 || nullIdx === automated.length - 1) continue;
|
|
1315
|
+
const nullEntry = automated[nullIdx];
|
|
1316
|
+
issues.push({
|
|
1317
|
+
severity: "warning",
|
|
1318
|
+
code: "null-criterion-not-last",
|
|
1319
|
+
message: `Transition "${nullEntry.t.name}" on state "${stateCode}" is automated and has no criterion, so it always fires; later automated transitions on this state are unreachable.`,
|
|
1320
|
+
...transitionTargetId(doc, wf.name, stateCode, nullEntry.index),
|
|
1321
|
+
detail: {
|
|
1322
|
+
workflow: wf.name,
|
|
1323
|
+
state: stateCode,
|
|
1324
|
+
transitionName: nullEntry.t.name
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
for (let j = nullIdx + 1; j < automated.length; j++) {
|
|
1328
|
+
const dead = automated[j];
|
|
1329
|
+
issues.push({
|
|
1330
|
+
severity: "warning",
|
|
1331
|
+
code: "unreachable-automated-transition",
|
|
1332
|
+
message: `Transition "${dead.t.name}" on state "${stateCode}" is unreachable: an earlier automated transition ("${nullEntry.t.name}") has no criterion and will always fire first.`,
|
|
1333
|
+
...transitionTargetId(doc, wf.name, stateCode, dead.index),
|
|
1334
|
+
detail: {
|
|
1335
|
+
workflow: wf.name,
|
|
1336
|
+
state: stateCode,
|
|
1337
|
+
transitionName: dead.t.name,
|
|
1338
|
+
blockedBy: nullEntry.t.name
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return issues;
|
|
1345
|
+
}
|
|
923
1346
|
|
|
924
1347
|
// src/validate/schema.ts
|
|
925
1348
|
function zodErrorToIssues(err) {
|
|
@@ -934,6 +1357,8 @@ function zodErrorToIssues(err) {
|
|
|
934
1357
|
}
|
|
935
1358
|
|
|
936
1359
|
// src/parse/parse-import.ts
|
|
1360
|
+
var MAX_JSON_BYTES = 5 * 1024 * 1024;
|
|
1361
|
+
var MAX_JSON_OBJECT_DEPTH = 200;
|
|
937
1362
|
function parseJsonSafe(json) {
|
|
938
1363
|
try {
|
|
939
1364
|
return { ok: true, value: JSON.parse(json) };
|
|
@@ -941,11 +1366,36 @@ function parseJsonSafe(json) {
|
|
|
941
1366
|
return { ok: false, err: e.message };
|
|
942
1367
|
}
|
|
943
1368
|
}
|
|
1369
|
+
function exceedsObjectDepth(value, limit) {
|
|
1370
|
+
const stack = [{ val: value, depth: 1 }];
|
|
1371
|
+
while (stack.length > 0) {
|
|
1372
|
+
const { val, depth } = stack.pop();
|
|
1373
|
+
if (depth > limit) return true;
|
|
1374
|
+
if (typeof val !== "object" || val === null) continue;
|
|
1375
|
+
const children = Array.isArray(val) ? val : Object.values(val);
|
|
1376
|
+
for (const child of children) {
|
|
1377
|
+
if (typeof child === "object" && child !== null) {
|
|
1378
|
+
stack.push({ val: child, depth: depth + 1 });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
944
1384
|
function parseImportPayload(json, prior) {
|
|
1385
|
+
if (json.length > MAX_JSON_BYTES) {
|
|
1386
|
+
throw new ParseJsonError(
|
|
1387
|
+
`Workflow JSON exceeds the maximum allowed size of ${MAX_JSON_BYTES / (1024 * 1024)} MB.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
945
1390
|
const parsed = parseJsonSafe(json);
|
|
946
1391
|
if (!parsed.ok) {
|
|
947
1392
|
throw new ParseJsonError(`Invalid JSON: ${parsed.err}`);
|
|
948
1393
|
}
|
|
1394
|
+
if (exceedsObjectDepth(parsed.value, MAX_JSON_OBJECT_DEPTH)) {
|
|
1395
|
+
throw new ParseJsonError(
|
|
1396
|
+
`Workflow JSON nesting depth exceeds the maximum allowed depth of ${MAX_JSON_OBJECT_DEPTH}.`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
949
1399
|
let aliased;
|
|
950
1400
|
try {
|
|
951
1401
|
aliased = normalizeOperatorAlias(parsed.value);
|
|
@@ -961,7 +1411,7 @@ function parseImportPayload(json, prior) {
|
|
|
961
1411
|
]
|
|
962
1412
|
};
|
|
963
1413
|
}
|
|
964
|
-
const schemaResult = ImportPayloadSchema.safeParse(aliased);
|
|
1414
|
+
const schemaResult = ImportPayloadSchema.safeParse(coerceCanonicalDefaults(aliased));
|
|
965
1415
|
if (!schemaResult.success) {
|
|
966
1416
|
return { ok: false, issues: zodErrorToIssues(schemaResult.error) };
|
|
967
1417
|
}
|
|
@@ -982,6 +1432,53 @@ function parseImportPayload(json, prior) {
|
|
|
982
1432
|
issues
|
|
983
1433
|
};
|
|
984
1434
|
}
|
|
1435
|
+
function coerceCanonicalDefaults(value) {
|
|
1436
|
+
if (!isObj(value)) return value;
|
|
1437
|
+
const v = value;
|
|
1438
|
+
if (!Array.isArray(v["workflows"])) return value;
|
|
1439
|
+
return {
|
|
1440
|
+
...v,
|
|
1441
|
+
workflows: v["workflows"].map((wf) => {
|
|
1442
|
+
if (!isObj(wf)) return wf;
|
|
1443
|
+
const w = wf;
|
|
1444
|
+
if (!isObj(w["states"])) return wf;
|
|
1445
|
+
const states = w["states"];
|
|
1446
|
+
const nextStates = {};
|
|
1447
|
+
for (const [code, state] of Object.entries(states)) {
|
|
1448
|
+
if (!isObj(state)) {
|
|
1449
|
+
nextStates[code] = state;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const s = state;
|
|
1453
|
+
if (!Array.isArray(s["transitions"])) {
|
|
1454
|
+
nextStates[code] = state;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
nextStates[code] = {
|
|
1458
|
+
...s,
|
|
1459
|
+
transitions: s["transitions"].map((t) => {
|
|
1460
|
+
if (!isObj(t)) return t;
|
|
1461
|
+
const tx = t;
|
|
1462
|
+
if (!Array.isArray(tx["processors"])) return t;
|
|
1463
|
+
return {
|
|
1464
|
+
...tx,
|
|
1465
|
+
processors: tx["processors"].map((p) => {
|
|
1466
|
+
if (!isObj(p)) return p;
|
|
1467
|
+
const proc = p;
|
|
1468
|
+
if (typeof proc["type"] === "string") return p;
|
|
1469
|
+
return { type: "externalized", ...proc };
|
|
1470
|
+
})
|
|
1471
|
+
};
|
|
1472
|
+
})
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
return { ...w, states: nextStates };
|
|
1476
|
+
})
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function isObj(v) {
|
|
1480
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1481
|
+
}
|
|
985
1482
|
|
|
986
1483
|
// src/parse/parse-export.ts
|
|
987
1484
|
function parseExportPayload(json, prior) {
|
|
@@ -1132,7 +1629,11 @@ function outputCriterion(c) {
|
|
|
1132
1629
|
jsonPath: c.jsonPath,
|
|
1133
1630
|
operation: c.operation
|
|
1134
1631
|
};
|
|
1135
|
-
if (c.
|
|
1632
|
+
if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
|
|
1633
|
+
out["value"] = null;
|
|
1634
|
+
} else if (c.value !== void 0) {
|
|
1635
|
+
out["value"] = c.value;
|
|
1636
|
+
}
|
|
1136
1637
|
return out;
|
|
1137
1638
|
}
|
|
1138
1639
|
case "group":
|
|
@@ -1158,7 +1659,11 @@ function outputCriterion(c) {
|
|
|
1158
1659
|
field: c.field,
|
|
1159
1660
|
operation: c.operation
|
|
1160
1661
|
};
|
|
1161
|
-
if (c.
|
|
1662
|
+
if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
|
|
1663
|
+
out["value"] = null;
|
|
1664
|
+
} else if (c.value !== void 0) {
|
|
1665
|
+
out["value"] = c.value;
|
|
1666
|
+
}
|
|
1162
1667
|
return out;
|
|
1163
1668
|
}
|
|
1164
1669
|
case "array":
|
|
@@ -1191,8 +1696,9 @@ function outputExternalizedProcessor(p) {
|
|
|
1191
1696
|
type: "externalized",
|
|
1192
1697
|
name: p.name
|
|
1193
1698
|
};
|
|
1194
|
-
|
|
1195
|
-
|
|
1699
|
+
out["executionMode"] = p.executionMode ?? "ASYNC_NEW_TX";
|
|
1700
|
+
if ("startNewTxOnDispatch" in p && p.startNewTxOnDispatch !== void 0) {
|
|
1701
|
+
out["startNewTxOnDispatch"] = p.startNewTxOnDispatch;
|
|
1196
1702
|
}
|
|
1197
1703
|
if (p.config !== void 0) {
|
|
1198
1704
|
const cfg = outputExternalizedConfig(p.config);
|
|
@@ -1387,45 +1893,6 @@ function walkPath(root, path) {
|
|
|
1387
1893
|
return null;
|
|
1388
1894
|
}
|
|
1389
1895
|
|
|
1390
|
-
// src/identity/id-for.ts
|
|
1391
|
-
function idFor2(meta, ref) {
|
|
1392
|
-
switch (ref.kind) {
|
|
1393
|
-
case "workflow":
|
|
1394
|
-
return meta.ids.workflows[ref.workflow] ?? null;
|
|
1395
|
-
case "state": {
|
|
1396
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
|
|
1397
|
-
if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
|
|
1398
|
-
}
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
case "transition": {
|
|
1402
|
-
const matches = [];
|
|
1403
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
1404
|
-
if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
|
|
1405
|
-
matches.push(uuid);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
return matches[ref.ordinal] ?? null;
|
|
1409
|
-
}
|
|
1410
|
-
case "processor": {
|
|
1411
|
-
const matches = [];
|
|
1412
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
|
|
1413
|
-
if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
|
|
1414
|
-
}
|
|
1415
|
-
return matches[ref.ordinal] ?? null;
|
|
1416
|
-
}
|
|
1417
|
-
case "criterion": {
|
|
1418
|
-
const target = JSON.stringify(ref.path);
|
|
1419
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
|
|
1420
|
-
if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
|
|
1421
|
-
return uuid;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
return null;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
1896
|
// src/validate/index.ts
|
|
1430
1897
|
function validateImportSchema(raw) {
|
|
1431
1898
|
const parsed = ImportPayloadSchema.safeParse(raw);
|
|
@@ -1445,9 +1912,13 @@ function validateSession(session) {
|
|
|
1445
1912
|
// src/patch/apply.ts
|
|
1446
1913
|
var import_immer = require("immer");
|
|
1447
1914
|
function applyPatch(doc, patch) {
|
|
1448
|
-
if (patch.op === "setEdgeAnchors")
|
|
1449
|
-
|
|
1450
|
-
|
|
1915
|
+
if (patch.op === "setEdgeAnchors") return applySetEdgeAnchors(doc, patch);
|
|
1916
|
+
if (patch.op === "setNodePosition") return applySetNodePosition(doc, patch);
|
|
1917
|
+
if (patch.op === "removeNodePosition") return applyRemoveNodePosition(doc, patch);
|
|
1918
|
+
if (patch.op === "resetLayout") return applyResetLayout(doc, patch);
|
|
1919
|
+
if (patch.op === "addComment") return applyAddComment(doc, patch);
|
|
1920
|
+
if (patch.op === "updateComment") return applyUpdateComment(doc, patch);
|
|
1921
|
+
if (patch.op === "removeComment") return applyRemoveComment(doc, patch);
|
|
1451
1922
|
const nextSession = (0, import_immer.produce)(doc.session, (d) => {
|
|
1452
1923
|
const draft = d;
|
|
1453
1924
|
switch (patch.op) {
|
|
@@ -1493,6 +1964,11 @@ function applyPatch(doc, patch) {
|
|
|
1493
1964
|
case "renameState": {
|
|
1494
1965
|
const wf = draft.workflows.find((w) => w.name === patch.workflow);
|
|
1495
1966
|
if (!wf) return;
|
|
1967
|
+
if (patch.to !== patch.from && patch.to in wf.states) {
|
|
1968
|
+
throw new PatchConflictError(
|
|
1969
|
+
`State "${patch.to}" already exists in workflow "${patch.workflow}"`
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1496
1972
|
renameStateCascading(wf, patch.from, patch.to);
|
|
1497
1973
|
return;
|
|
1498
1974
|
}
|
|
@@ -1540,6 +2016,23 @@ function applyPatch(doc, patch) {
|
|
|
1540
2016
|
state.transitions.splice(patch.toIndex, 0, item);
|
|
1541
2017
|
return;
|
|
1542
2018
|
}
|
|
2019
|
+
case "moveTransitionSource": {
|
|
2020
|
+
const wf = draft.workflows.find((w) => w.name === patch.workflow);
|
|
2021
|
+
if (!wf) return;
|
|
2022
|
+
const fromState = wf.states[patch.fromState];
|
|
2023
|
+
const toState = wf.states[patch.toState];
|
|
2024
|
+
if (!fromState || !toState) return;
|
|
2025
|
+
const idx = fromState.transitions.findIndex((t) => t.name === patch.transitionName);
|
|
2026
|
+
if (idx < 0) return;
|
|
2027
|
+
if (patch.fromState !== patch.toState && toState.transitions.some((t) => t.name === patch.transitionName)) {
|
|
2028
|
+
throw new PatchConflictError(
|
|
2029
|
+
`Transition "${patch.transitionName}" already exists in state "${patch.toState}"`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
const [transition] = fromState.transitions.splice(idx, 1);
|
|
2033
|
+
if (transition) toState.transitions.push(transition);
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
1543
2036
|
case "addProcessor": {
|
|
1544
2037
|
const loc = locateTransition(doc, patch.transitionUuid);
|
|
1545
2038
|
if (!loc) return;
|
|
@@ -1622,9 +2115,11 @@ function applyPatch(doc, patch) {
|
|
|
1622
2115
|
}
|
|
1623
2116
|
});
|
|
1624
2117
|
const nextMeta = assignSyntheticIds(nextSession, doc.meta);
|
|
2118
|
+
preserveMovedTransitionUuid(doc, patch, nextSession, nextMeta);
|
|
2119
|
+
const cleanedWorkflowUi = cleanupWorkflowUi(nextMeta.workflowUi, nextSession, nextMeta);
|
|
1625
2120
|
return {
|
|
1626
2121
|
session: nextSession,
|
|
1627
|
-
meta: { ...nextMeta, revision: doc.meta.revision + 1 }
|
|
2122
|
+
meta: { ...nextMeta, workflowUi: cleanedWorkflowUi, revision: doc.meta.revision + 1 }
|
|
1628
2123
|
};
|
|
1629
2124
|
}
|
|
1630
2125
|
function applyPatches(doc, patches) {
|
|
@@ -1657,6 +2152,124 @@ function applySetEdgeAnchors(doc, patch) {
|
|
|
1657
2152
|
}
|
|
1658
2153
|
};
|
|
1659
2154
|
}
|
|
2155
|
+
function applySetNodePosition(doc, patch) {
|
|
2156
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2157
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2158
|
+
const nodes = { ...current.layout?.nodes ?? {} };
|
|
2159
|
+
nodes[patch.stateCode] = { x: patch.x, y: patch.y, pinned: patch.pinned ?? true };
|
|
2160
|
+
workflowUi[patch.workflow] = { ...current, layout: { nodes } };
|
|
2161
|
+
return {
|
|
2162
|
+
session: doc.session,
|
|
2163
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function applyRemoveNodePosition(doc, patch) {
|
|
2167
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2168
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2169
|
+
const nodes = { ...current.layout?.nodes ?? {} };
|
|
2170
|
+
delete nodes[patch.stateCode];
|
|
2171
|
+
workflowUi[patch.workflow] = {
|
|
2172
|
+
...current,
|
|
2173
|
+
layout: Object.keys(nodes).length > 0 ? { nodes } : void 0
|
|
2174
|
+
};
|
|
2175
|
+
return {
|
|
2176
|
+
session: doc.session,
|
|
2177
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function applyResetLayout(doc, patch) {
|
|
2181
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2182
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2183
|
+
workflowUi[patch.workflow] = { ...current, layout: void 0 };
|
|
2184
|
+
return {
|
|
2185
|
+
session: doc.session,
|
|
2186
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
function applyAddComment(doc, patch) {
|
|
2190
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2191
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2192
|
+
const comments = { ...current.comments ?? {}, [patch.comment.id]: patch.comment };
|
|
2193
|
+
workflowUi[patch.workflow] = { ...current, comments };
|
|
2194
|
+
return {
|
|
2195
|
+
session: doc.session,
|
|
2196
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
function applyUpdateComment(doc, patch) {
|
|
2200
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2201
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2202
|
+
const existing = current.comments?.[patch.commentId];
|
|
2203
|
+
if (!existing) return { ...doc, meta: { ...doc.meta, revision: doc.meta.revision + 1 } };
|
|
2204
|
+
const updated = { ...existing, ...patch.updates, id: existing.id };
|
|
2205
|
+
const comments = { ...current.comments ?? {}, [patch.commentId]: updated };
|
|
2206
|
+
workflowUi[patch.workflow] = { ...current, comments };
|
|
2207
|
+
return {
|
|
2208
|
+
session: doc.session,
|
|
2209
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
function applyRemoveComment(doc, patch) {
|
|
2213
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2214
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2215
|
+
const comments = { ...current.comments ?? {} };
|
|
2216
|
+
delete comments[patch.commentId];
|
|
2217
|
+
workflowUi[patch.workflow] = {
|
|
2218
|
+
...current,
|
|
2219
|
+
comments: Object.keys(comments).length > 0 ? comments : void 0
|
|
2220
|
+
};
|
|
2221
|
+
return {
|
|
2222
|
+
session: doc.session,
|
|
2223
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
function preserveMovedTransitionUuid(priorDoc, patch, nextSession, nextMeta) {
|
|
2227
|
+
if (patch.op !== "moveTransitionSource") return;
|
|
2228
|
+
const oldUuid = transitionUuidByName(
|
|
2229
|
+
priorDoc,
|
|
2230
|
+
patch.workflow,
|
|
2231
|
+
patch.fromState,
|
|
2232
|
+
patch.transitionName
|
|
2233
|
+
);
|
|
2234
|
+
if (!oldUuid) return;
|
|
2235
|
+
const newUuid = transitionUuidByNameInSession(
|
|
2236
|
+
nextSession,
|
|
2237
|
+
nextMeta,
|
|
2238
|
+
patch.workflow,
|
|
2239
|
+
patch.toState,
|
|
2240
|
+
patch.transitionName
|
|
2241
|
+
);
|
|
2242
|
+
if (!newUuid || newUuid === oldUuid) return;
|
|
2243
|
+
const newPtr = nextMeta.ids.transitions[newUuid];
|
|
2244
|
+
if (!newPtr) return;
|
|
2245
|
+
delete nextMeta.ids.transitions[newUuid];
|
|
2246
|
+
nextMeta.ids.transitions[oldUuid] = {
|
|
2247
|
+
...newPtr,
|
|
2248
|
+
transitionUuid: oldUuid
|
|
2249
|
+
};
|
|
2250
|
+
for (const processorPtr of Object.values(nextMeta.ids.processors)) {
|
|
2251
|
+
if (processorPtr.transitionUuid === newUuid) {
|
|
2252
|
+
processorPtr.transitionUuid = oldUuid;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
for (const criterionPtr of Object.values(nextMeta.ids.criteria)) {
|
|
2256
|
+
const host = criterionPtr.host;
|
|
2257
|
+
if ((host.kind === "transition" || host.kind === "processorConfig") && host.transitionUuid === newUuid) {
|
|
2258
|
+
host.transitionUuid = oldUuid;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function transitionUuidByName(doc, workflow, state, transitionName) {
|
|
2263
|
+
return transitionUuidByNameInSession(doc.session, doc.meta, workflow, state, transitionName);
|
|
2264
|
+
}
|
|
2265
|
+
function transitionUuidByNameInSession(session, meta, workflow, state, transitionName) {
|
|
2266
|
+
const wf = session.workflows.find((candidate) => candidate.name === workflow);
|
|
2267
|
+
const transitions = wf?.states[state]?.transitions ?? [];
|
|
2268
|
+
const index = transitions.findIndex((transition) => transition.name === transitionName);
|
|
2269
|
+
if (index < 0) return null;
|
|
2270
|
+
const ordered = Object.entries(meta.ids.transitions).filter(([, ptr]) => ptr.workflow === workflow && ptr.state === state).map(([uuid]) => uuid);
|
|
2271
|
+
return ordered[index] ?? null;
|
|
2272
|
+
}
|
|
1660
2273
|
function renameStateCascading(wf, from, to) {
|
|
1661
2274
|
if (!(from in wf.states) || from === to) return;
|
|
1662
2275
|
wf.states[to] = wf.states[from];
|
|
@@ -1705,6 +2318,65 @@ function locateProcessor(doc, processorUuid) {
|
|
|
1705
2318
|
processorIndex: pIdx
|
|
1706
2319
|
};
|
|
1707
2320
|
}
|
|
2321
|
+
function cleanupWorkflowUi(workflowUi, session, meta) {
|
|
2322
|
+
const wfNames = new Set(session.workflows.map((w) => w.name));
|
|
2323
|
+
const result = {};
|
|
2324
|
+
const validTransitionIdsByWorkflow = /* @__PURE__ */ new Map();
|
|
2325
|
+
if (meta) {
|
|
2326
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
2327
|
+
let set = validTransitionIdsByWorkflow.get(ptr.workflow);
|
|
2328
|
+
if (!set) {
|
|
2329
|
+
set = /* @__PURE__ */ new Set();
|
|
2330
|
+
validTransitionIdsByWorkflow.set(ptr.workflow, set);
|
|
2331
|
+
}
|
|
2332
|
+
set.add(uuid);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
for (const [wfName, ui] of Object.entries(workflowUi)) {
|
|
2336
|
+
if (!wfNames.has(wfName)) continue;
|
|
2337
|
+
const wf = session.workflows.find((w) => w.name === wfName);
|
|
2338
|
+
const existingStates = wf ? new Set(Object.keys(wf.states)) : /* @__PURE__ */ new Set();
|
|
2339
|
+
const allTransitionNames = /* @__PURE__ */ new Set();
|
|
2340
|
+
if (wf) {
|
|
2341
|
+
for (const state of Object.values(wf.states)) {
|
|
2342
|
+
for (const t of state.transitions) allTransitionNames.add(t.name);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
let layout = ui.layout;
|
|
2346
|
+
if (layout?.nodes) {
|
|
2347
|
+
const cleanNodes = Object.fromEntries(
|
|
2348
|
+
Object.entries(layout.nodes).filter(([code]) => existingStates.has(code))
|
|
2349
|
+
);
|
|
2350
|
+
layout = Object.keys(cleanNodes).length > 0 ? { nodes: cleanNodes } : void 0;
|
|
2351
|
+
}
|
|
2352
|
+
let comments = ui.comments;
|
|
2353
|
+
if (comments) {
|
|
2354
|
+
const cleanComments = {};
|
|
2355
|
+
for (const [id, c] of Object.entries(comments)) {
|
|
2356
|
+
if (c.attachedTo?.kind === "state" && !existingStates.has(c.attachedTo.stateCode)) {
|
|
2357
|
+
cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
|
|
2358
|
+
} else if (c.attachedTo?.kind === "transition" && !allTransitionNames.has(c.attachedTo.transitionName)) {
|
|
2359
|
+
cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
|
|
2360
|
+
} else {
|
|
2361
|
+
cleanComments[id] = c;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
comments = Object.keys(cleanComments).length > 0 ? cleanComments : void 0;
|
|
2365
|
+
}
|
|
2366
|
+
let edgeAnchors = ui.edgeAnchors;
|
|
2367
|
+
if (edgeAnchors && meta) {
|
|
2368
|
+
const validTransitionIds = validTransitionIdsByWorkflow.get(wfName) ?? /* @__PURE__ */ new Set();
|
|
2369
|
+
const cleanAnchors = Object.fromEntries(
|
|
2370
|
+
Object.entries(edgeAnchors).filter(
|
|
2371
|
+
([transitionUuid]) => validTransitionIds.has(transitionUuid)
|
|
2372
|
+
)
|
|
2373
|
+
);
|
|
2374
|
+
edgeAnchors = Object.keys(cleanAnchors).length > 0 ? cleanAnchors : void 0;
|
|
2375
|
+
}
|
|
2376
|
+
result[wfName] = { ...ui, layout, comments, edgeAnchors };
|
|
2377
|
+
}
|
|
2378
|
+
return result;
|
|
2379
|
+
}
|
|
1708
2380
|
function applyCriterionAtPath(container, path, criterion) {
|
|
1709
2381
|
if (path.length === 0) return;
|
|
1710
2382
|
let node = container;
|
|
@@ -1741,7 +2413,6 @@ function invertPatch(doc, patch) {
|
|
|
1741
2413
|
case "removeWorkflow":
|
|
1742
2414
|
case "renameWorkflow":
|
|
1743
2415
|
case "removeState":
|
|
1744
|
-
case "renameState":
|
|
1745
2416
|
case "replaceSession":
|
|
1746
2417
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1747
2418
|
case "updateWorkflowMeta": {
|
|
@@ -1756,30 +2427,19 @@ function invertPatch(doc, patch) {
|
|
|
1756
2427
|
case "setInitialState": {
|
|
1757
2428
|
const wf = findWorkflow(doc, patch.workflow);
|
|
1758
2429
|
if (!wf) return noop();
|
|
1759
|
-
return {
|
|
1760
|
-
op: "setInitialState",
|
|
1761
|
-
workflow: patch.workflow,
|
|
1762
|
-
stateCode: wf.initialState
|
|
1763
|
-
};
|
|
2430
|
+
return { op: "setInitialState", workflow: patch.workflow, stateCode: wf.initialState };
|
|
1764
2431
|
}
|
|
1765
2432
|
case "setWorkflowCriterion": {
|
|
1766
2433
|
const wf = findWorkflow(doc, patch.workflow);
|
|
1767
2434
|
if (!wf) return noop();
|
|
1768
|
-
return wf.criterion ? {
|
|
1769
|
-
op: "setWorkflowCriterion",
|
|
1770
|
-
workflow: patch.workflow,
|
|
1771
|
-
criterion: cloneCriterion(wf.criterion)
|
|
1772
|
-
} : { op: "setWorkflowCriterion", workflow: patch.workflow };
|
|
2435
|
+
return wf.criterion ? { op: "setWorkflowCriterion", workflow: patch.workflow, criterion: cloneCriterion(wf.criterion) } : { op: "setWorkflowCriterion", workflow: patch.workflow };
|
|
1773
2436
|
}
|
|
1774
2437
|
case "addState":
|
|
1775
|
-
return {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
};
|
|
1780
|
-
case "addTransition": {
|
|
2438
|
+
return { op: "removeState", workflow: patch.workflow, stateCode: patch.stateCode };
|
|
2439
|
+
case "renameState":
|
|
2440
|
+
return { op: "renameState", workflow: patch.workflow, from: patch.to, to: patch.from };
|
|
2441
|
+
case "addTransition":
|
|
1781
2442
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1782
|
-
}
|
|
1783
2443
|
case "updateTransition": {
|
|
1784
2444
|
const t = findTransition(doc, patch.transitionUuid);
|
|
1785
2445
|
if (!t) return noop();
|
|
@@ -1787,15 +2447,48 @@ function invertPatch(doc, patch) {
|
|
|
1787
2447
|
for (const key of Object.keys(patch.updates)) {
|
|
1788
2448
|
prior[key] = t[key];
|
|
1789
2449
|
}
|
|
2450
|
+
return { op: "updateTransition", transitionUuid: patch.transitionUuid, updates: prior };
|
|
2451
|
+
}
|
|
2452
|
+
case "removeTransition": {
|
|
2453
|
+
const loc = locateTransition2(doc, patch.transitionUuid);
|
|
2454
|
+
if (!loc) return noop();
|
|
2455
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2456
|
+
const state = wf?.states[loc.state];
|
|
2457
|
+
const t = state?.transitions[loc.index];
|
|
2458
|
+
if (!t) return noop();
|
|
1790
2459
|
return {
|
|
1791
|
-
op: "
|
|
1792
|
-
|
|
1793
|
-
|
|
2460
|
+
op: "addTransition",
|
|
2461
|
+
workflow: loc.workflow,
|
|
2462
|
+
fromState: loc.state,
|
|
2463
|
+
transition: structuredClone(t)
|
|
1794
2464
|
};
|
|
1795
2465
|
}
|
|
1796
|
-
case "
|
|
1797
|
-
|
|
1798
|
-
|
|
2466
|
+
case "reorderTransition": {
|
|
2467
|
+
const loc = locateTransition2(doc, patch.transitionUuid);
|
|
2468
|
+
if (!loc) return noop();
|
|
2469
|
+
const orderedForState = [];
|
|
2470
|
+
for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
|
|
2471
|
+
if (p.workflow === patch.workflow && p.state === patch.fromState) {
|
|
2472
|
+
orderedForState.push(uuid);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
const uuidAtTarget = orderedForState[patch.toIndex] ?? patch.transitionUuid;
|
|
2476
|
+
return {
|
|
2477
|
+
op: "reorderTransition",
|
|
2478
|
+
workflow: patch.workflow,
|
|
2479
|
+
fromState: patch.fromState,
|
|
2480
|
+
transitionUuid: uuidAtTarget,
|
|
2481
|
+
toIndex: loc.index
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
case "moveTransitionSource":
|
|
2485
|
+
return {
|
|
2486
|
+
op: "moveTransitionSource",
|
|
2487
|
+
workflow: patch.workflow,
|
|
2488
|
+
fromState: patch.toState,
|
|
2489
|
+
toState: patch.fromState,
|
|
2490
|
+
transitionName: patch.transitionName
|
|
2491
|
+
};
|
|
1799
2492
|
case "addProcessor":
|
|
1800
2493
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1801
2494
|
case "updateProcessor": {
|
|
@@ -1805,23 +2498,43 @@ function invertPatch(doc, patch) {
|
|
|
1805
2498
|
for (const key of Object.keys(patch.updates)) {
|
|
1806
2499
|
prior[key] = p[key];
|
|
1807
2500
|
}
|
|
2501
|
+
return { op: "updateProcessor", processorUuid: patch.processorUuid, updates: prior };
|
|
2502
|
+
}
|
|
2503
|
+
case "removeProcessor": {
|
|
2504
|
+
const ptr = doc.meta.ids.processors[patch.processorUuid];
|
|
2505
|
+
if (!ptr) return noop();
|
|
2506
|
+
const procLoc = locateProcessor2(doc, patch.processorUuid);
|
|
2507
|
+
if (!procLoc) return noop();
|
|
2508
|
+
const wf = findWorkflow(doc, procLoc.workflow);
|
|
2509
|
+
const state = wf?.states[procLoc.state];
|
|
2510
|
+
const t = state?.transitions[procLoc.transitionIndex];
|
|
2511
|
+
const p = t?.processors?.[procLoc.processorIndex];
|
|
2512
|
+
if (!p) return noop();
|
|
1808
2513
|
return {
|
|
1809
|
-
op: "
|
|
1810
|
-
|
|
1811
|
-
|
|
2514
|
+
op: "addProcessor",
|
|
2515
|
+
transitionUuid: ptr.transitionUuid,
|
|
2516
|
+
processor: structuredClone(p),
|
|
2517
|
+
index: procLoc.processorIndex
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
case "reorderProcessor": {
|
|
2521
|
+
const procLoc = locateProcessor2(doc, patch.processorUuid);
|
|
2522
|
+
if (!procLoc) return noop();
|
|
2523
|
+
const orderedForTransition = [];
|
|
2524
|
+
for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
|
|
2525
|
+
if (p.transitionUuid === patch.transitionUuid) orderedForTransition.push(uuid);
|
|
2526
|
+
}
|
|
2527
|
+
const uuidAtTarget = orderedForTransition[patch.toIndex] ?? patch.processorUuid;
|
|
2528
|
+
return {
|
|
2529
|
+
op: "reorderProcessor",
|
|
2530
|
+
transitionUuid: patch.transitionUuid,
|
|
2531
|
+
processorUuid: uuidAtTarget,
|
|
2532
|
+
toIndex: procLoc.processorIndex
|
|
1812
2533
|
};
|
|
1813
2534
|
}
|
|
1814
|
-
case "removeProcessor":
|
|
1815
|
-
case "reorderProcessor":
|
|
1816
|
-
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1817
2535
|
case "setCriterion": {
|
|
1818
2536
|
const prior = readCriterionAt(doc, patch.host, patch.path);
|
|
1819
|
-
return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : {
|
|
1820
|
-
op: "setCriterion",
|
|
1821
|
-
host: patch.host,
|
|
1822
|
-
path: patch.path,
|
|
1823
|
-
criterion: cloneCriterion(prior)
|
|
1824
|
-
};
|
|
2537
|
+
return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : { op: "setCriterion", host: patch.host, path: patch.path, criterion: cloneCriterion(prior) };
|
|
1825
2538
|
}
|
|
1826
2539
|
case "setImportMode":
|
|
1827
2540
|
return { op: "setImportMode", mode: doc.session.importMode };
|
|
@@ -1837,6 +2550,36 @@ function invertPatch(doc, patch) {
|
|
|
1837
2550
|
anchors: prior ? { ...prior } : null
|
|
1838
2551
|
};
|
|
1839
2552
|
}
|
|
2553
|
+
case "setNodePosition": {
|
|
2554
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
|
|
2555
|
+
if (!prior) {
|
|
2556
|
+
return { op: "removeNodePosition", workflow: patch.workflow, stateCode: patch.stateCode };
|
|
2557
|
+
}
|
|
2558
|
+
return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
|
|
2559
|
+
}
|
|
2560
|
+
case "removeNodePosition": {
|
|
2561
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
|
|
2562
|
+
if (!prior) return noop();
|
|
2563
|
+
return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
|
|
2564
|
+
}
|
|
2565
|
+
case "resetLayout":
|
|
2566
|
+
return noop();
|
|
2567
|
+
case "addComment":
|
|
2568
|
+
return { op: "removeComment", workflow: patch.workflow, commentId: patch.comment.id };
|
|
2569
|
+
case "updateComment": {
|
|
2570
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
|
|
2571
|
+
if (!prior) return noop();
|
|
2572
|
+
const priorUpdates = {};
|
|
2573
|
+
for (const key of Object.keys(patch.updates)) {
|
|
2574
|
+
priorUpdates[key] = prior[key];
|
|
2575
|
+
}
|
|
2576
|
+
return { op: "updateComment", workflow: patch.workflow, commentId: patch.commentId, updates: priorUpdates };
|
|
2577
|
+
}
|
|
2578
|
+
case "removeComment": {
|
|
2579
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
|
|
2580
|
+
if (!prior) return noop();
|
|
2581
|
+
return { op: "addComment", workflow: patch.workflow, comment: structuredClone(prior) };
|
|
2582
|
+
}
|
|
1840
2583
|
}
|
|
1841
2584
|
}
|
|
1842
2585
|
function noop() {
|
|
@@ -1845,30 +2588,42 @@ function noop() {
|
|
|
1845
2588
|
function findWorkflow(doc, name) {
|
|
1846
2589
|
return doc.session.workflows.find((w) => w.name === name);
|
|
1847
2590
|
}
|
|
1848
|
-
function
|
|
2591
|
+
function locateTransition2(doc, transitionUuid) {
|
|
1849
2592
|
const ptr = doc.meta.ids.transitions[transitionUuid];
|
|
1850
|
-
if (!ptr) return
|
|
1851
|
-
const wf = findWorkflow(doc, ptr.workflow);
|
|
1852
|
-
const state = wf?.states[ptr.state];
|
|
1853
|
-
if (!state) return void 0;
|
|
2593
|
+
if (!ptr) return null;
|
|
1854
2594
|
const ordered = [];
|
|
1855
2595
|
for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
|
|
1856
2596
|
if (p.workflow === ptr.workflow && p.state === ptr.state) ordered.push(uuid);
|
|
1857
2597
|
}
|
|
1858
2598
|
const idx = ordered.indexOf(transitionUuid);
|
|
1859
|
-
return
|
|
2599
|
+
if (idx < 0) return null;
|
|
2600
|
+
return { workflow: ptr.workflow, state: ptr.state, index: idx };
|
|
1860
2601
|
}
|
|
1861
|
-
function
|
|
2602
|
+
function locateProcessor2(doc, processorUuid) {
|
|
1862
2603
|
const ptr = doc.meta.ids.processors[processorUuid];
|
|
1863
|
-
if (!ptr) return
|
|
1864
|
-
const
|
|
1865
|
-
if (!
|
|
2604
|
+
if (!ptr) return null;
|
|
2605
|
+
const tLoc = locateTransition2(doc, ptr.transitionUuid);
|
|
2606
|
+
if (!tLoc) return null;
|
|
1866
2607
|
const ordered = [];
|
|
1867
2608
|
for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
|
|
1868
2609
|
if (p.transitionUuid === ptr.transitionUuid) ordered.push(uuid);
|
|
1869
2610
|
}
|
|
1870
|
-
const
|
|
1871
|
-
return
|
|
2611
|
+
const pIdx = ordered.indexOf(processorUuid);
|
|
2612
|
+
if (pIdx < 0) return null;
|
|
2613
|
+
return { workflow: tLoc.workflow, state: tLoc.state, transitionIndex: tLoc.index, processorIndex: pIdx };
|
|
2614
|
+
}
|
|
2615
|
+
function findTransition(doc, transitionUuid) {
|
|
2616
|
+
const loc = locateTransition2(doc, transitionUuid);
|
|
2617
|
+
if (!loc) return void 0;
|
|
2618
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2619
|
+
return wf?.states[loc.state]?.transitions[loc.index];
|
|
2620
|
+
}
|
|
2621
|
+
function findProcessor(doc, processorUuid) {
|
|
2622
|
+
const loc = locateProcessor2(doc, processorUuid);
|
|
2623
|
+
if (!loc) return void 0;
|
|
2624
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2625
|
+
const t = wf?.states[loc.state]?.transitions[loc.transitionIndex];
|
|
2626
|
+
return t?.processors?.[loc.processorIndex];
|
|
1872
2627
|
}
|
|
1873
2628
|
function readCriterionAt(doc, host, path) {
|
|
1874
2629
|
const wf = findWorkflow(doc, host.workflow);
|
|
@@ -1903,6 +2658,26 @@ function cloneCriterion(c) {
|
|
|
1903
2658
|
return structuredClone(c);
|
|
1904
2659
|
}
|
|
1905
2660
|
|
|
2661
|
+
// src/patch/transaction.ts
|
|
2662
|
+
function applyTransaction(doc, tx) {
|
|
2663
|
+
return tx.patches.reduce((d, p) => applyPatch(d, p), doc);
|
|
2664
|
+
}
|
|
2665
|
+
function invertTransaction(doc, tx) {
|
|
2666
|
+
if (tx.inverses.length > 0) {
|
|
2667
|
+
return {
|
|
2668
|
+
summary: `Undo: ${tx.summary}`,
|
|
2669
|
+
patches: tx.inverses,
|
|
2670
|
+
inverses: tx.patches
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
const computed = tx.patches.map((p) => invertPatch(doc, p)).reverse();
|
|
2674
|
+
return {
|
|
2675
|
+
summary: `Undo: ${tx.summary}`,
|
|
2676
|
+
patches: computed,
|
|
2677
|
+
inverses: tx.patches
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
|
|
1906
2681
|
// src/migrate/registry.ts
|
|
1907
2682
|
var registry = [];
|
|
1908
2683
|
function registerMigration(entry) {
|
|
@@ -1940,9 +2715,60 @@ function migrateSession(session, from, to) {
|
|
|
1940
2715
|
return path.reduce((s, entry) => entry.migrate(s), session);
|
|
1941
2716
|
}
|
|
1942
2717
|
registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
|
|
2718
|
+
|
|
2719
|
+
// src/criteria/describe.ts
|
|
2720
|
+
var OPERATOR_SYMBOL = {
|
|
2721
|
+
EQUALS: "=",
|
|
2722
|
+
NOT_EQUAL: "\u2260",
|
|
2723
|
+
GREATER_THAN: ">",
|
|
2724
|
+
LESS_THAN: "<",
|
|
2725
|
+
GREATER_OR_EQUAL: "\u2265",
|
|
2726
|
+
LESS_OR_EQUAL: "\u2264",
|
|
2727
|
+
IEQUALS: "=",
|
|
2728
|
+
INOT_EQUAL: "\u2260"
|
|
2729
|
+
};
|
|
2730
|
+
function describeCriterion(c) {
|
|
2731
|
+
switch (c.type) {
|
|
2732
|
+
case "simple":
|
|
2733
|
+
return describeBinary(c.jsonPath, c.operation, c.value);
|
|
2734
|
+
case "group": {
|
|
2735
|
+
const n = c.conditions.length;
|
|
2736
|
+
return `${c.operator} (${n} condition${n === 1 ? "" : "s"})`;
|
|
2737
|
+
}
|
|
2738
|
+
case "function":
|
|
2739
|
+
return `Function: ${c.function.name || "<unnamed>"}`;
|
|
2740
|
+
case "lifecycle":
|
|
2741
|
+
return describeBinary(c.field, c.operation, c.value);
|
|
2742
|
+
case "array": {
|
|
2743
|
+
const n = c.value.length;
|
|
2744
|
+
return `${c.jsonPath} ${c.operation} [${n} value${n === 1 ? "" : "s"}]`;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function describeBinary(lhs, op, value) {
|
|
2749
|
+
if (op === "IS_NULL") return `${lhs} IS NULL`;
|
|
2750
|
+
if (op === "NOT_NULL") return `${lhs} IS NOT NULL`;
|
|
2751
|
+
if (op === "IS_CHANGED") return `${lhs} CHANGED`;
|
|
2752
|
+
if (op === "IS_UNCHANGED") return `${lhs} UNCHANGED`;
|
|
2753
|
+
if (op === "BETWEEN" || op === "BETWEEN_INCLUSIVE") {
|
|
2754
|
+
const inclusive = op === "BETWEEN_INCLUSIVE";
|
|
2755
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
2756
|
+
return `${lhs} \u2208 ${inclusive ? "[" : "("}${formatValue(value[0])}, ${formatValue(value[1])}${inclusive ? "]" : ")"}`;
|
|
2757
|
+
}
|
|
2758
|
+
return `${lhs} ${op} ${formatValue(value)}`;
|
|
2759
|
+
}
|
|
2760
|
+
const symbol = OPERATOR_SYMBOL[op] ?? op;
|
|
2761
|
+
return `${lhs} ${symbol} ${formatValue(value)}`;
|
|
2762
|
+
}
|
|
2763
|
+
function formatValue(v) {
|
|
2764
|
+
if (v === void 0) return "?";
|
|
2765
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
2766
|
+
return JSON.stringify(v);
|
|
2767
|
+
}
|
|
1943
2768
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1944
2769
|
0 && (module.exports = {
|
|
1945
2770
|
ArrayCriterionSchema,
|
|
2771
|
+
CRITERION_DEPTH_WARNING_THRESHOLD,
|
|
1946
2772
|
CriterionSchema,
|
|
1947
2773
|
ExecutionModeSchema,
|
|
1948
2774
|
ExportPayloadSchema,
|
|
@@ -1952,26 +2778,38 @@ registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
|
|
|
1952
2778
|
GroupCriterionSchema,
|
|
1953
2779
|
ImportPayloadSchema,
|
|
1954
2780
|
LifecycleCriterionSchema,
|
|
2781
|
+
MAX_CRITERION_DEPTH,
|
|
2782
|
+
MAX_JSON_BYTES,
|
|
2783
|
+
MAX_JSON_OBJECT_DEPTH,
|
|
1955
2784
|
NAME_REGEX,
|
|
1956
2785
|
NameSchema,
|
|
2786
|
+
OPERATOR_GROUPS,
|
|
1957
2787
|
OPERATOR_TYPES,
|
|
2788
|
+
OPERATOR_VALUE_SHAPE,
|
|
1958
2789
|
OperatorEnum,
|
|
1959
2790
|
ParseJsonError,
|
|
2791
|
+
PatchConflictError,
|
|
1960
2792
|
ProcessorSchema,
|
|
2793
|
+
SUPPORTED_GROUP_OPERATORS,
|
|
2794
|
+
SUPPORTED_SIMPLE_OPERATORS,
|
|
1961
2795
|
ScheduledProcessorSchema,
|
|
1962
2796
|
SchemaError,
|
|
1963
2797
|
SimpleCriterionSchema,
|
|
1964
2798
|
StateSchema,
|
|
1965
2799
|
TransitionSchema,
|
|
2800
|
+
UNSUPPORTED_OPERATORS,
|
|
1966
2801
|
WorkflowApiConflictError,
|
|
1967
2802
|
WorkflowApiTransportError,
|
|
1968
2803
|
WorkflowSchema,
|
|
1969
2804
|
applyPatch,
|
|
1970
2805
|
applyPatches,
|
|
2806
|
+
applyTransaction,
|
|
1971
2807
|
assignSyntheticIds,
|
|
2808
|
+
describeCriterion,
|
|
1972
2809
|
findMigrationPath,
|
|
1973
2810
|
idFor,
|
|
1974
2811
|
invertPatch,
|
|
2812
|
+
invertTransaction,
|
|
1975
2813
|
listMigrations,
|
|
1976
2814
|
lookupById,
|
|
1977
2815
|
migrateSession,
|
|
@@ -1997,6 +2835,7 @@ registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
|
|
|
1997
2835
|
validateAll,
|
|
1998
2836
|
validateExportSchema,
|
|
1999
2837
|
validateImportSchema,
|
|
2838
|
+
validateJsonPathSubset,
|
|
2000
2839
|
validateSemantics,
|
|
2001
2840
|
validateSession,
|
|
2002
2841
|
zodErrorToIssues
|