@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.js
CHANGED
|
@@ -30,6 +30,14 @@ var OPERATOR_TYPES = /* @__PURE__ */ new Set([
|
|
|
30
30
|
"IS_CHANGED"
|
|
31
31
|
]);
|
|
32
32
|
|
|
33
|
+
// src/types/transaction.ts
|
|
34
|
+
var PatchConflictError = class extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "PatchConflictError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
33
41
|
// src/types/api.ts
|
|
34
42
|
var WorkflowApiConflictError = class extends Error {
|
|
35
43
|
constructor(entity, serverConcurrencyToken, message = "Workflow save conflict: server state has changed.") {
|
|
@@ -144,11 +152,17 @@ var FunctionCriterionSchema = z3.lazy(
|
|
|
144
152
|
|
|
145
153
|
// src/schema/processor.ts
|
|
146
154
|
import { z as z4 } from "zod";
|
|
147
|
-
var ExecutionModeSchema = z4.enum([
|
|
155
|
+
var ExecutionModeSchema = z4.enum([
|
|
156
|
+
"SYNC",
|
|
157
|
+
"ASYNC_SAME_TX",
|
|
158
|
+
"ASYNC_NEW_TX",
|
|
159
|
+
"COMMIT_BEFORE_DISPATCH"
|
|
160
|
+
]);
|
|
148
161
|
var ExternalizedProcessorSchema = z4.object({
|
|
149
162
|
type: z4.literal("externalized"),
|
|
150
163
|
name: NameSchema,
|
|
151
164
|
executionMode: ExecutionModeSchema.optional(),
|
|
165
|
+
startNewTxOnDispatch: z4.boolean().optional(),
|
|
152
166
|
config: FunctionConfigSchema.and(
|
|
153
167
|
z4.object({
|
|
154
168
|
asyncResult: z4.boolean().optional(),
|
|
@@ -176,7 +190,7 @@ var TransitionSchema = z5.object({
|
|
|
176
190
|
name: NameSchema,
|
|
177
191
|
next: NameSchema,
|
|
178
192
|
manual: z5.boolean(),
|
|
179
|
-
disabled: z5.boolean(),
|
|
193
|
+
disabled: z5.boolean().default(false),
|
|
180
194
|
criterion: CriterionSchema.optional(),
|
|
181
195
|
processors: z5.array(ProcessorSchema).optional()
|
|
182
196
|
});
|
|
@@ -389,6 +403,118 @@ function mintCriterionIds(c, host, path, ids) {
|
|
|
389
403
|
}
|
|
390
404
|
}
|
|
391
405
|
|
|
406
|
+
// src/criteria/operators.ts
|
|
407
|
+
var SUPPORTED_SIMPLE_OPERATORS = /* @__PURE__ */ new Set([
|
|
408
|
+
"EQUALS",
|
|
409
|
+
"NOT_EQUAL",
|
|
410
|
+
"GREATER_THAN",
|
|
411
|
+
"LESS_THAN",
|
|
412
|
+
"GREATER_OR_EQUAL",
|
|
413
|
+
"LESS_OR_EQUAL",
|
|
414
|
+
"CONTAINS",
|
|
415
|
+
"NOT_CONTAINS",
|
|
416
|
+
"STARTS_WITH",
|
|
417
|
+
"NOT_STARTS_WITH",
|
|
418
|
+
"ENDS_WITH",
|
|
419
|
+
"NOT_ENDS_WITH",
|
|
420
|
+
"LIKE",
|
|
421
|
+
"IS_NULL",
|
|
422
|
+
"NOT_NULL",
|
|
423
|
+
"BETWEEN",
|
|
424
|
+
"BETWEEN_INCLUSIVE",
|
|
425
|
+
"MATCHES_PATTERN",
|
|
426
|
+
"IEQUALS",
|
|
427
|
+
"INOT_EQUAL",
|
|
428
|
+
"ICONTAINS",
|
|
429
|
+
"INOT_CONTAINS",
|
|
430
|
+
"ISTARTS_WITH",
|
|
431
|
+
"INOT_STARTS_WITH",
|
|
432
|
+
"IENDS_WITH",
|
|
433
|
+
"INOT_ENDS_WITH"
|
|
434
|
+
]);
|
|
435
|
+
var UNSUPPORTED_OPERATORS = /* @__PURE__ */ new Set([
|
|
436
|
+
"IS_UNCHANGED",
|
|
437
|
+
"IS_CHANGED"
|
|
438
|
+
]);
|
|
439
|
+
var SUPPORTED_GROUP_OPERATORS = ["AND", "OR"];
|
|
440
|
+
var MAX_CRITERION_DEPTH = 50;
|
|
441
|
+
var CRITERION_DEPTH_WARNING_THRESHOLD = 5;
|
|
442
|
+
var OPERATOR_GROUPS = [
|
|
443
|
+
{
|
|
444
|
+
id: "equality",
|
|
445
|
+
label: "Equality",
|
|
446
|
+
operators: ["EQUALS", "NOT_EQUAL", "IEQUALS", "INOT_EQUAL"]
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "ordering",
|
|
450
|
+
label: "Ordering",
|
|
451
|
+
operators: ["GREATER_THAN", "LESS_THAN", "GREATER_OR_EQUAL", "LESS_OR_EQUAL"]
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "range",
|
|
455
|
+
label: "Range",
|
|
456
|
+
operators: ["BETWEEN", "BETWEEN_INCLUSIVE"]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: "substring",
|
|
460
|
+
label: "Substring",
|
|
461
|
+
operators: [
|
|
462
|
+
"CONTAINS",
|
|
463
|
+
"NOT_CONTAINS",
|
|
464
|
+
"ICONTAINS",
|
|
465
|
+
"INOT_CONTAINS",
|
|
466
|
+
"STARTS_WITH",
|
|
467
|
+
"NOT_STARTS_WITH",
|
|
468
|
+
"ISTARTS_WITH",
|
|
469
|
+
"INOT_STARTS_WITH",
|
|
470
|
+
"ENDS_WITH",
|
|
471
|
+
"NOT_ENDS_WITH",
|
|
472
|
+
"IENDS_WITH",
|
|
473
|
+
"INOT_ENDS_WITH"
|
|
474
|
+
]
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
id: "pattern",
|
|
478
|
+
label: "Pattern",
|
|
479
|
+
operators: ["LIKE", "MATCHES_PATTERN"]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "null",
|
|
483
|
+
label: "Null",
|
|
484
|
+
operators: ["IS_NULL", "NOT_NULL"]
|
|
485
|
+
}
|
|
486
|
+
];
|
|
487
|
+
var OPERATOR_VALUE_SHAPE = {
|
|
488
|
+
EQUALS: "scalar",
|
|
489
|
+
NOT_EQUAL: "scalar",
|
|
490
|
+
GREATER_THAN: "scalar",
|
|
491
|
+
LESS_THAN: "scalar",
|
|
492
|
+
GREATER_OR_EQUAL: "scalar",
|
|
493
|
+
LESS_OR_EQUAL: "scalar",
|
|
494
|
+
CONTAINS: "scalar",
|
|
495
|
+
NOT_CONTAINS: "scalar",
|
|
496
|
+
STARTS_WITH: "scalar",
|
|
497
|
+
NOT_STARTS_WITH: "scalar",
|
|
498
|
+
ENDS_WITH: "scalar",
|
|
499
|
+
NOT_ENDS_WITH: "scalar",
|
|
500
|
+
LIKE: "scalar",
|
|
501
|
+
MATCHES_PATTERN: "scalar",
|
|
502
|
+
IEQUALS: "scalar",
|
|
503
|
+
INOT_EQUAL: "scalar",
|
|
504
|
+
ICONTAINS: "scalar",
|
|
505
|
+
INOT_CONTAINS: "scalar",
|
|
506
|
+
ISTARTS_WITH: "scalar",
|
|
507
|
+
INOT_STARTS_WITH: "scalar",
|
|
508
|
+
IENDS_WITH: "scalar",
|
|
509
|
+
INOT_ENDS_WITH: "scalar",
|
|
510
|
+
BETWEEN: "range",
|
|
511
|
+
BETWEEN_INCLUSIVE: "range",
|
|
512
|
+
IS_NULL: "none",
|
|
513
|
+
NOT_NULL: "none",
|
|
514
|
+
IS_UNCHANGED: "none",
|
|
515
|
+
IS_CHANGED: "none"
|
|
516
|
+
};
|
|
517
|
+
|
|
392
518
|
// src/normalize/input.ts
|
|
393
519
|
function normalizeWorkflowInput(workflow) {
|
|
394
520
|
const out = {
|
|
@@ -431,12 +557,21 @@ function normalizeWorkflowInput(workflow) {
|
|
|
431
557
|
}
|
|
432
558
|
return out;
|
|
433
559
|
}
|
|
434
|
-
function normalizeCriterion(criterion) {
|
|
560
|
+
function normalizeCriterion(criterion, depth = 0) {
|
|
561
|
+
if (depth >= MAX_CRITERION_DEPTH) {
|
|
562
|
+
return criterion;
|
|
563
|
+
}
|
|
435
564
|
switch (criterion.type) {
|
|
436
565
|
case "simple":
|
|
566
|
+
if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
|
|
567
|
+
return { ...criterion, value: null };
|
|
568
|
+
}
|
|
437
569
|
return criterion;
|
|
438
570
|
case "group":
|
|
439
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
...criterion,
|
|
573
|
+
conditions: criterion.conditions.map((c) => normalizeCriterion(c, depth + 1))
|
|
574
|
+
};
|
|
440
575
|
case "function": {
|
|
441
576
|
const fn = criterion.function;
|
|
442
577
|
const out = {
|
|
@@ -444,12 +579,15 @@ function normalizeCriterion(criterion) {
|
|
|
444
579
|
function: {
|
|
445
580
|
name: fn.name.trim(),
|
|
446
581
|
...fn.config !== void 0 ? { config: fn.config } : {},
|
|
447
|
-
...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion) } : {}
|
|
582
|
+
...fn.criterion !== void 0 ? { criterion: normalizeCriterion(fn.criterion, depth + 1) } : {}
|
|
448
583
|
}
|
|
449
584
|
};
|
|
450
585
|
return out;
|
|
451
586
|
}
|
|
452
587
|
case "lifecycle":
|
|
588
|
+
if (criterion.operation === "IS_NULL" || criterion.operation === "NOT_NULL") {
|
|
589
|
+
return { ...criterion, value: null };
|
|
590
|
+
}
|
|
453
591
|
return criterion;
|
|
454
592
|
case "array":
|
|
455
593
|
return criterion;
|
|
@@ -466,6 +604,79 @@ function normalizeProcessor(p) {
|
|
|
466
604
|
};
|
|
467
605
|
}
|
|
468
606
|
|
|
607
|
+
// src/criteria/jsonPathSubset.ts
|
|
608
|
+
var SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
609
|
+
var INDEX_RE = /^(?:\d+|\*)$/;
|
|
610
|
+
function validateJsonPathSubset(path) {
|
|
611
|
+
if (path.length === 0) return { ok: false, reason: "empty" };
|
|
612
|
+
if (path[0] !== "$") return { ok: false, reason: "missing-root" };
|
|
613
|
+
if (path === "$") return { ok: true };
|
|
614
|
+
let i = 1;
|
|
615
|
+
while (i < path.length) {
|
|
616
|
+
const ch = path[i];
|
|
617
|
+
if (ch === ".") {
|
|
618
|
+
if (path[i + 1] === ".") return { ok: false, reason: "recursive-descent" };
|
|
619
|
+
i += 1;
|
|
620
|
+
const start = i;
|
|
621
|
+
while (i < path.length && path[i] !== "." && path[i] !== "[") i += 1;
|
|
622
|
+
const segment = path.slice(start, i);
|
|
623
|
+
if (!SEGMENT_RE.test(segment)) return { ok: false, reason: "malformed" };
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (ch === "[") {
|
|
627
|
+
if (path[i + 1] === "?") return { ok: false, reason: "filter-expression" };
|
|
628
|
+
if (path[i + 1] === "'" || path[i + 1] === '"') return { ok: false, reason: "malformed" };
|
|
629
|
+
const end = path.indexOf("]", i);
|
|
630
|
+
if (end === -1) return { ok: false, reason: "malformed" };
|
|
631
|
+
const inner = path.slice(i + 1, end);
|
|
632
|
+
if (!INDEX_RE.test(inner)) return { ok: false, reason: "malformed" };
|
|
633
|
+
i = end + 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
return { ok: false, reason: "malformed" };
|
|
637
|
+
}
|
|
638
|
+
return { ok: true };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/identity/id-for.ts
|
|
642
|
+
function idFor(meta, ref) {
|
|
643
|
+
switch (ref.kind) {
|
|
644
|
+
case "workflow":
|
|
645
|
+
return meta.ids.workflows[ref.workflow] ?? null;
|
|
646
|
+
case "state": {
|
|
647
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
|
|
648
|
+
if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
case "transition": {
|
|
653
|
+
const matches = [];
|
|
654
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
655
|
+
if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
|
|
656
|
+
matches.push(uuid);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return matches[ref.ordinal] ?? null;
|
|
660
|
+
}
|
|
661
|
+
case "processor": {
|
|
662
|
+
const matches = [];
|
|
663
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
|
|
664
|
+
if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
|
|
665
|
+
}
|
|
666
|
+
return matches[ref.ordinal] ?? null;
|
|
667
|
+
}
|
|
668
|
+
case "criterion": {
|
|
669
|
+
const target = JSON.stringify(ref.path);
|
|
670
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
|
|
671
|
+
if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
|
|
672
|
+
return uuid;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
469
680
|
// src/validate/helpers.ts
|
|
470
681
|
function isValidName(name) {
|
|
471
682
|
return NAME_REGEX.test(name);
|
|
@@ -491,12 +702,15 @@ function* walkCriteria(session) {
|
|
|
491
702
|
}
|
|
492
703
|
}
|
|
493
704
|
}
|
|
494
|
-
function* walkInner(c, where) {
|
|
705
|
+
function* walkInner(c, where, depth = 0) {
|
|
495
706
|
yield { criterion: c, where };
|
|
707
|
+
if (depth >= MAX_CRITERION_DEPTH) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
496
710
|
if (c.type === "group") {
|
|
497
|
-
for (const child of c.conditions) yield* walkInner(child, where);
|
|
711
|
+
for (const child of c.conditions) yield* walkInner(child, where, depth + 1);
|
|
498
712
|
} else if (c.type === "function" && c.function.criterion) {
|
|
499
|
-
yield* walkInner(c.function.criterion, where);
|
|
713
|
+
yield* walkInner(c.function.criterion, where, depth + 1);
|
|
500
714
|
}
|
|
501
715
|
}
|
|
502
716
|
|
|
@@ -509,6 +723,8 @@ function validateSemantics(session, doc) {
|
|
|
509
723
|
issues.push(...validateWorkflow(wf, doc));
|
|
510
724
|
}
|
|
511
725
|
issues.push(...criterionRules(session));
|
|
726
|
+
issues.push(...criterionDepthRules(session));
|
|
727
|
+
issues.push(...automatedOrderingRules(session, doc));
|
|
512
728
|
if (session.workflows.length === 1) {
|
|
513
729
|
const only = session.workflows[0];
|
|
514
730
|
if (only && only.criterion !== void 0) {
|
|
@@ -546,14 +762,14 @@ function validateWorkflow(wf, doc) {
|
|
|
546
762
|
severity: "error",
|
|
547
763
|
code: "missing-initial-state",
|
|
548
764
|
message: `Workflow "${wf.name}" has no initialState.`,
|
|
549
|
-
...
|
|
765
|
+
...idFor2(doc, wf.name, "workflow")
|
|
550
766
|
});
|
|
551
767
|
} else if (!(wf.initialState in wf.states)) {
|
|
552
768
|
issues.push({
|
|
553
769
|
severity: "error",
|
|
554
770
|
code: "unknown-initial-state",
|
|
555
771
|
message: `Workflow "${wf.name}" initialState "${wf.initialState}" is not a state.`,
|
|
556
|
-
...
|
|
772
|
+
...idFor2(doc, wf.name, "workflow")
|
|
557
773
|
});
|
|
558
774
|
}
|
|
559
775
|
if (!isValidName(wf.name)) {
|
|
@@ -606,6 +822,13 @@ function validateWorkflow(wf, doc) {
|
|
|
606
822
|
message: `Scheduled processor "${p.name}" has empty target transition.`
|
|
607
823
|
});
|
|
608
824
|
}
|
|
825
|
+
if (p.type === "externalized" && p.startNewTxOnDispatch === true && p.executionMode !== "COMMIT_BEFORE_DISPATCH") {
|
|
826
|
+
issues.push({
|
|
827
|
+
severity: "warning",
|
|
828
|
+
code: "start-new-tx-without-commit-before-dispatch",
|
|
829
|
+
message: `Processor "${p.name}" sets startNewTxOnDispatch but executionMode is not COMMIT_BEFORE_DISPATCH.`
|
|
830
|
+
});
|
|
831
|
+
}
|
|
609
832
|
if (p.type === "externalized" && p.config) {
|
|
610
833
|
if (p.config.crossoverToAsyncMs !== void 0 && p.config.asyncResult !== true) {
|
|
611
834
|
issues.push({
|
|
@@ -748,7 +971,16 @@ function criterionRules(session) {
|
|
|
748
971
|
});
|
|
749
972
|
}
|
|
750
973
|
break;
|
|
751
|
-
case "array":
|
|
974
|
+
case "array": {
|
|
975
|
+
const arrPathCheck = validateJsonPathSubset(criterion.jsonPath);
|
|
976
|
+
if (!arrPathCheck.ok) {
|
|
977
|
+
issues.push({
|
|
978
|
+
severity: "error",
|
|
979
|
+
code: "invalid-jsonpath-subset",
|
|
980
|
+
message: `Array criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${arrPathCheck.reason}) (at ${describe(where)}).`,
|
|
981
|
+
detail: { jsonPath: criterion.jsonPath, reason: arrPathCheck.reason }
|
|
982
|
+
});
|
|
983
|
+
}
|
|
752
984
|
for (const v of criterion.value) {
|
|
753
985
|
if (typeof v !== "string") {
|
|
754
986
|
issues.push({
|
|
@@ -760,6 +992,7 @@ function criterionRules(session) {
|
|
|
760
992
|
}
|
|
761
993
|
}
|
|
762
994
|
break;
|
|
995
|
+
}
|
|
763
996
|
case "lifecycle":
|
|
764
997
|
if (!LIFECYCLE_FIELDS.has(criterion.field)) {
|
|
765
998
|
issues.push({
|
|
@@ -768,22 +1001,144 @@ function criterionRules(session) {
|
|
|
768
1001
|
message: `Lifecycle criterion field "${criterion.field}" is invalid.`
|
|
769
1002
|
});
|
|
770
1003
|
}
|
|
1004
|
+
if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
|
|
1005
|
+
issues.push({
|
|
1006
|
+
severity: "warning",
|
|
1007
|
+
code: "unsupported-operator",
|
|
1008
|
+
message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
|
|
1009
|
+
detail: { operation: criterion.operation }
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
771
1012
|
break;
|
|
772
1013
|
case "group":
|
|
773
|
-
if (criterion.operator === "NOT"
|
|
1014
|
+
if (criterion.operator === "NOT") {
|
|
774
1015
|
issues.push({
|
|
775
1016
|
severity: "warning",
|
|
776
|
-
code: "
|
|
777
|
-
message: `NOT
|
|
1017
|
+
code: "unsupported-group-operator",
|
|
1018
|
+
message: `Group operator "NOT" is not implemented by the engine (at ${describe(where)}).`,
|
|
1019
|
+
detail: { operator: "NOT" }
|
|
778
1020
|
});
|
|
1021
|
+
if (criterion.conditions.length > 1) {
|
|
1022
|
+
issues.push({
|
|
1023
|
+
severity: "warning",
|
|
1024
|
+
code: "not-with-multiple-conditions",
|
|
1025
|
+
message: `NOT group has ${criterion.conditions.length} conditions; should have exactly one.`
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
779
1028
|
}
|
|
780
1029
|
break;
|
|
781
|
-
case "simple":
|
|
1030
|
+
case "simple": {
|
|
1031
|
+
const pathCheck = validateJsonPathSubset(criterion.jsonPath);
|
|
1032
|
+
if (!pathCheck.ok) {
|
|
1033
|
+
issues.push({
|
|
1034
|
+
severity: "error",
|
|
1035
|
+
code: "invalid-jsonpath-subset",
|
|
1036
|
+
message: `Simple criterion jsonPath "${criterion.jsonPath}" is not in the supported subset (${pathCheck.reason}) (at ${describe(where)}).`,
|
|
1037
|
+
detail: { jsonPath: criterion.jsonPath, reason: pathCheck.reason }
|
|
1038
|
+
});
|
|
1039
|
+
} else if (criterion.jsonPath.startsWith("$._meta")) {
|
|
1040
|
+
issues.push({
|
|
1041
|
+
severity: "warning",
|
|
1042
|
+
code: "lifecycle-path-in-simple",
|
|
1043
|
+
message: `Simple criterion path "${criterion.jsonPath}" looks like a lifecycle path; use a lifecycle criterion instead (at ${describe(where)}).`,
|
|
1044
|
+
detail: { jsonPath: criterion.jsonPath }
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
if (UNSUPPORTED_OPERATORS.has(criterion.operation)) {
|
|
1048
|
+
issues.push({
|
|
1049
|
+
severity: "warning",
|
|
1050
|
+
code: "unsupported-operator",
|
|
1051
|
+
message: `Operator "${criterion.operation}" is not implemented by the engine (at ${describe(where)}).`,
|
|
1052
|
+
detail: { operation: criterion.operation }
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (criterion.operation === "BETWEEN" || criterion.operation === "BETWEEN_INCLUSIVE") {
|
|
1056
|
+
if (!Array.isArray(criterion.value) || criterion.value.length !== 2) {
|
|
1057
|
+
issues.push({
|
|
1058
|
+
severity: "error",
|
|
1059
|
+
code: "simple-between-shape",
|
|
1060
|
+
message: `Operator "${criterion.operation}" requires a two-element [low, high] array value (at ${describe(where)}).`,
|
|
1061
|
+
detail: { operation: criterion.operation }
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (criterion.operation === "LIKE" && typeof criterion.value === "string" && /[%_]/.test(criterion.value)) {
|
|
1066
|
+
issues.push({
|
|
1067
|
+
severity: "warning",
|
|
1068
|
+
code: "like-wildcard-warning",
|
|
1069
|
+
message: `LIKE pattern contains "%" or "_" which are always wildcards (no escape mechanism) (at ${describe(where)}).`
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (criterion.operation === "MATCHES_PATTERN" && typeof criterion.value === "string" && criterion.value.length > 0 && !criterion.value.startsWith("^") && !criterion.value.endsWith("$")) {
|
|
1073
|
+
issues.push({
|
|
1074
|
+
severity: "warning",
|
|
1075
|
+
code: "matches-pattern-unanchored",
|
|
1076
|
+
message: `MATCHES_PATTERN regex is unanchored; include "^"/"$" for whole-string match (at ${describe(where)}).`
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
782
1079
|
break;
|
|
1080
|
+
}
|
|
783
1081
|
}
|
|
784
1082
|
}
|
|
785
1083
|
return issues;
|
|
786
1084
|
}
|
|
1085
|
+
function criterionDepthRules(session) {
|
|
1086
|
+
const issues = [];
|
|
1087
|
+
for (const wf of session.workflows) {
|
|
1088
|
+
if (wf.criterion) {
|
|
1089
|
+
pushDepthIssue(issues, criterionMaxDepth(wf.criterion), {
|
|
1090
|
+
kind: "workflow",
|
|
1091
|
+
workflow: wf.name
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
for (const [stateCode, state] of Object.entries(wf.states)) {
|
|
1095
|
+
for (let i = 0; i < state.transitions.length; i++) {
|
|
1096
|
+
const t = state.transitions[i];
|
|
1097
|
+
if (!t.criterion) continue;
|
|
1098
|
+
pushDepthIssue(issues, criterionMaxDepth(t.criterion), {
|
|
1099
|
+
kind: "transition",
|
|
1100
|
+
workflow: wf.name,
|
|
1101
|
+
state: stateCode,
|
|
1102
|
+
transitionIndex: i,
|
|
1103
|
+
transitionName: t.name
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return issues;
|
|
1109
|
+
}
|
|
1110
|
+
function pushDepthIssue(issues, maxDepth, where) {
|
|
1111
|
+
if (maxDepth >= MAX_CRITERION_DEPTH) {
|
|
1112
|
+
issues.push({
|
|
1113
|
+
severity: "error",
|
|
1114
|
+
code: "criterion-depth-limit",
|
|
1115
|
+
message: `Criterion tree depth ${maxDepth} exceeds engine limit ${MAX_CRITERION_DEPTH} (at ${describe(where)}).`,
|
|
1116
|
+
detail: { maxDepth, threshold: MAX_CRITERION_DEPTH }
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
if (maxDepth >= CRITERION_DEPTH_WARNING_THRESHOLD) {
|
|
1120
|
+
issues.push({
|
|
1121
|
+
severity: "warning",
|
|
1122
|
+
code: "criterion-depth-warning",
|
|
1123
|
+
message: `Criterion tree depth ${maxDepth} is hard to read; consider flattening (at ${describe(where)}).`,
|
|
1124
|
+
detail: { maxDepth, threshold: CRITERION_DEPTH_WARNING_THRESHOLD }
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function criterionMaxDepth(root) {
|
|
1129
|
+
const stack = [{ node: root, depth: 1 }];
|
|
1130
|
+
let max = 0;
|
|
1131
|
+
while (stack.length > 0) {
|
|
1132
|
+
const { node, depth } = stack.pop();
|
|
1133
|
+
if (depth > max) max = depth;
|
|
1134
|
+
if (node.type === "group") {
|
|
1135
|
+
for (const child of node.conditions) stack.push({ node: child, depth: depth + 1 });
|
|
1136
|
+
} else if (node.type === "function" && node.function.criterion) {
|
|
1137
|
+
stack.push({ node: node.function.criterion, depth: depth + 1 });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return max;
|
|
1141
|
+
}
|
|
787
1142
|
function reachableStates(wf) {
|
|
788
1143
|
const visited = /* @__PURE__ */ new Set();
|
|
789
1144
|
if (!(wf.initialState in wf.states)) return visited;
|
|
@@ -832,11 +1187,65 @@ function describe(w) {
|
|
|
832
1187
|
if (w.kind === "workflow") return `workflow "${w.workflow}"`;
|
|
833
1188
|
return `transition "${w.transitionName}" on "${w.workflow}:${w.state}"`;
|
|
834
1189
|
}
|
|
835
|
-
function
|
|
1190
|
+
function idFor2(doc, workflowName, _kind) {
|
|
836
1191
|
if (!doc) return {};
|
|
837
1192
|
const id = doc.meta.ids.workflows[workflowName];
|
|
838
1193
|
return id ? { targetId: id } : {};
|
|
839
1194
|
}
|
|
1195
|
+
function transitionTargetId(doc, workflow, state, declarationIndex) {
|
|
1196
|
+
if (!doc) return {};
|
|
1197
|
+
const id = idFor(doc.meta, {
|
|
1198
|
+
kind: "transition",
|
|
1199
|
+
workflow,
|
|
1200
|
+
state,
|
|
1201
|
+
transitionName: "",
|
|
1202
|
+
ordinal: declarationIndex
|
|
1203
|
+
});
|
|
1204
|
+
return id ? { targetId: id } : {};
|
|
1205
|
+
}
|
|
1206
|
+
function automatedOrderingRules(session, doc) {
|
|
1207
|
+
const issues = [];
|
|
1208
|
+
for (const wf of session.workflows) {
|
|
1209
|
+
for (const [stateCode, state] of Object.entries(wf.states)) {
|
|
1210
|
+
const automated = [];
|
|
1211
|
+
state.transitions.forEach((t, index) => {
|
|
1212
|
+
if (t.manual !== true && t.disabled !== true) {
|
|
1213
|
+
automated.push({ index, t });
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
const nullIdx = automated.findIndex(({ t }) => t.criterion === void 0);
|
|
1217
|
+
if (nullIdx === -1 || nullIdx === automated.length - 1) continue;
|
|
1218
|
+
const nullEntry = automated[nullIdx];
|
|
1219
|
+
issues.push({
|
|
1220
|
+
severity: "warning",
|
|
1221
|
+
code: "null-criterion-not-last",
|
|
1222
|
+
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.`,
|
|
1223
|
+
...transitionTargetId(doc, wf.name, stateCode, nullEntry.index),
|
|
1224
|
+
detail: {
|
|
1225
|
+
workflow: wf.name,
|
|
1226
|
+
state: stateCode,
|
|
1227
|
+
transitionName: nullEntry.t.name
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
for (let j = nullIdx + 1; j < automated.length; j++) {
|
|
1231
|
+
const dead = automated[j];
|
|
1232
|
+
issues.push({
|
|
1233
|
+
severity: "warning",
|
|
1234
|
+
code: "unreachable-automated-transition",
|
|
1235
|
+
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.`,
|
|
1236
|
+
...transitionTargetId(doc, wf.name, stateCode, dead.index),
|
|
1237
|
+
detail: {
|
|
1238
|
+
workflow: wf.name,
|
|
1239
|
+
state: stateCode,
|
|
1240
|
+
transitionName: dead.t.name,
|
|
1241
|
+
blockedBy: nullEntry.t.name
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return issues;
|
|
1248
|
+
}
|
|
840
1249
|
|
|
841
1250
|
// src/validate/schema.ts
|
|
842
1251
|
function zodErrorToIssues(err) {
|
|
@@ -851,6 +1260,8 @@ function zodErrorToIssues(err) {
|
|
|
851
1260
|
}
|
|
852
1261
|
|
|
853
1262
|
// src/parse/parse-import.ts
|
|
1263
|
+
var MAX_JSON_BYTES = 5 * 1024 * 1024;
|
|
1264
|
+
var MAX_JSON_OBJECT_DEPTH = 200;
|
|
854
1265
|
function parseJsonSafe(json) {
|
|
855
1266
|
try {
|
|
856
1267
|
return { ok: true, value: JSON.parse(json) };
|
|
@@ -858,11 +1269,36 @@ function parseJsonSafe(json) {
|
|
|
858
1269
|
return { ok: false, err: e.message };
|
|
859
1270
|
}
|
|
860
1271
|
}
|
|
1272
|
+
function exceedsObjectDepth(value, limit) {
|
|
1273
|
+
const stack = [{ val: value, depth: 1 }];
|
|
1274
|
+
while (stack.length > 0) {
|
|
1275
|
+
const { val, depth } = stack.pop();
|
|
1276
|
+
if (depth > limit) return true;
|
|
1277
|
+
if (typeof val !== "object" || val === null) continue;
|
|
1278
|
+
const children = Array.isArray(val) ? val : Object.values(val);
|
|
1279
|
+
for (const child of children) {
|
|
1280
|
+
if (typeof child === "object" && child !== null) {
|
|
1281
|
+
stack.push({ val: child, depth: depth + 1 });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
861
1287
|
function parseImportPayload(json, prior) {
|
|
1288
|
+
if (json.length > MAX_JSON_BYTES) {
|
|
1289
|
+
throw new ParseJsonError(
|
|
1290
|
+
`Workflow JSON exceeds the maximum allowed size of ${MAX_JSON_BYTES / (1024 * 1024)} MB.`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
862
1293
|
const parsed = parseJsonSafe(json);
|
|
863
1294
|
if (!parsed.ok) {
|
|
864
1295
|
throw new ParseJsonError(`Invalid JSON: ${parsed.err}`);
|
|
865
1296
|
}
|
|
1297
|
+
if (exceedsObjectDepth(parsed.value, MAX_JSON_OBJECT_DEPTH)) {
|
|
1298
|
+
throw new ParseJsonError(
|
|
1299
|
+
`Workflow JSON nesting depth exceeds the maximum allowed depth of ${MAX_JSON_OBJECT_DEPTH}.`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
866
1302
|
let aliased;
|
|
867
1303
|
try {
|
|
868
1304
|
aliased = normalizeOperatorAlias(parsed.value);
|
|
@@ -878,7 +1314,7 @@ function parseImportPayload(json, prior) {
|
|
|
878
1314
|
]
|
|
879
1315
|
};
|
|
880
1316
|
}
|
|
881
|
-
const schemaResult = ImportPayloadSchema.safeParse(aliased);
|
|
1317
|
+
const schemaResult = ImportPayloadSchema.safeParse(coerceCanonicalDefaults(aliased));
|
|
882
1318
|
if (!schemaResult.success) {
|
|
883
1319
|
return { ok: false, issues: zodErrorToIssues(schemaResult.error) };
|
|
884
1320
|
}
|
|
@@ -899,6 +1335,53 @@ function parseImportPayload(json, prior) {
|
|
|
899
1335
|
issues
|
|
900
1336
|
};
|
|
901
1337
|
}
|
|
1338
|
+
function coerceCanonicalDefaults(value) {
|
|
1339
|
+
if (!isObj(value)) return value;
|
|
1340
|
+
const v = value;
|
|
1341
|
+
if (!Array.isArray(v["workflows"])) return value;
|
|
1342
|
+
return {
|
|
1343
|
+
...v,
|
|
1344
|
+
workflows: v["workflows"].map((wf) => {
|
|
1345
|
+
if (!isObj(wf)) return wf;
|
|
1346
|
+
const w = wf;
|
|
1347
|
+
if (!isObj(w["states"])) return wf;
|
|
1348
|
+
const states = w["states"];
|
|
1349
|
+
const nextStates = {};
|
|
1350
|
+
for (const [code, state] of Object.entries(states)) {
|
|
1351
|
+
if (!isObj(state)) {
|
|
1352
|
+
nextStates[code] = state;
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const s = state;
|
|
1356
|
+
if (!Array.isArray(s["transitions"])) {
|
|
1357
|
+
nextStates[code] = state;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
nextStates[code] = {
|
|
1361
|
+
...s,
|
|
1362
|
+
transitions: s["transitions"].map((t) => {
|
|
1363
|
+
if (!isObj(t)) return t;
|
|
1364
|
+
const tx = t;
|
|
1365
|
+
if (!Array.isArray(tx["processors"])) return t;
|
|
1366
|
+
return {
|
|
1367
|
+
...tx,
|
|
1368
|
+
processors: tx["processors"].map((p) => {
|
|
1369
|
+
if (!isObj(p)) return p;
|
|
1370
|
+
const proc = p;
|
|
1371
|
+
if (typeof proc["type"] === "string") return p;
|
|
1372
|
+
return { type: "externalized", ...proc };
|
|
1373
|
+
})
|
|
1374
|
+
};
|
|
1375
|
+
})
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
return { ...w, states: nextStates };
|
|
1379
|
+
})
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function isObj(v) {
|
|
1383
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1384
|
+
}
|
|
902
1385
|
|
|
903
1386
|
// src/parse/parse-export.ts
|
|
904
1387
|
function parseExportPayload(json, prior) {
|
|
@@ -1049,7 +1532,11 @@ function outputCriterion(c) {
|
|
|
1049
1532
|
jsonPath: c.jsonPath,
|
|
1050
1533
|
operation: c.operation
|
|
1051
1534
|
};
|
|
1052
|
-
if (c.
|
|
1535
|
+
if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
|
|
1536
|
+
out["value"] = null;
|
|
1537
|
+
} else if (c.value !== void 0) {
|
|
1538
|
+
out["value"] = c.value;
|
|
1539
|
+
}
|
|
1053
1540
|
return out;
|
|
1054
1541
|
}
|
|
1055
1542
|
case "group":
|
|
@@ -1075,7 +1562,11 @@ function outputCriterion(c) {
|
|
|
1075
1562
|
field: c.field,
|
|
1076
1563
|
operation: c.operation
|
|
1077
1564
|
};
|
|
1078
|
-
if (c.
|
|
1565
|
+
if (c.operation === "IS_NULL" || c.operation === "NOT_NULL") {
|
|
1566
|
+
out["value"] = null;
|
|
1567
|
+
} else if (c.value !== void 0) {
|
|
1568
|
+
out["value"] = c.value;
|
|
1569
|
+
}
|
|
1079
1570
|
return out;
|
|
1080
1571
|
}
|
|
1081
1572
|
case "array":
|
|
@@ -1108,8 +1599,9 @@ function outputExternalizedProcessor(p) {
|
|
|
1108
1599
|
type: "externalized",
|
|
1109
1600
|
name: p.name
|
|
1110
1601
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1602
|
+
out["executionMode"] = p.executionMode ?? "ASYNC_NEW_TX";
|
|
1603
|
+
if ("startNewTxOnDispatch" in p && p.startNewTxOnDispatch !== void 0) {
|
|
1604
|
+
out["startNewTxOnDispatch"] = p.startNewTxOnDispatch;
|
|
1113
1605
|
}
|
|
1114
1606
|
if (p.config !== void 0) {
|
|
1115
1607
|
const cfg = outputExternalizedConfig(p.config);
|
|
@@ -1304,45 +1796,6 @@ function walkPath(root, path) {
|
|
|
1304
1796
|
return null;
|
|
1305
1797
|
}
|
|
1306
1798
|
|
|
1307
|
-
// src/identity/id-for.ts
|
|
1308
|
-
function idFor2(meta, ref) {
|
|
1309
|
-
switch (ref.kind) {
|
|
1310
|
-
case "workflow":
|
|
1311
|
-
return meta.ids.workflows[ref.workflow] ?? null;
|
|
1312
|
-
case "state": {
|
|
1313
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.states)) {
|
|
1314
|
-
if (ptr.workflow === ref.workflow && ptr.state === ref.state) return uuid;
|
|
1315
|
-
}
|
|
1316
|
-
return null;
|
|
1317
|
-
}
|
|
1318
|
-
case "transition": {
|
|
1319
|
-
const matches = [];
|
|
1320
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
1321
|
-
if (ptr.workflow === ref.workflow && ptr.state === ref.state) {
|
|
1322
|
-
matches.push(uuid);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
return matches[ref.ordinal] ?? null;
|
|
1326
|
-
}
|
|
1327
|
-
case "processor": {
|
|
1328
|
-
const matches = [];
|
|
1329
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.processors)) {
|
|
1330
|
-
if (ptr.transitionUuid === ref.transitionUuid) matches.push(uuid);
|
|
1331
|
-
}
|
|
1332
|
-
return matches[ref.ordinal] ?? null;
|
|
1333
|
-
}
|
|
1334
|
-
case "criterion": {
|
|
1335
|
-
const target = JSON.stringify(ref.path);
|
|
1336
|
-
for (const [uuid, ptr] of Object.entries(meta.ids.criteria)) {
|
|
1337
|
-
if (JSON.stringify(ptr.host) === JSON.stringify(ref.host) && JSON.stringify(ptr.path) === target) {
|
|
1338
|
-
return uuid;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
return null;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
1799
|
// src/validate/index.ts
|
|
1347
1800
|
function validateImportSchema(raw) {
|
|
1348
1801
|
const parsed = ImportPayloadSchema.safeParse(raw);
|
|
@@ -1362,9 +1815,13 @@ function validateSession(session) {
|
|
|
1362
1815
|
// src/patch/apply.ts
|
|
1363
1816
|
import { produce } from "immer";
|
|
1364
1817
|
function applyPatch(doc, patch) {
|
|
1365
|
-
if (patch.op === "setEdgeAnchors")
|
|
1366
|
-
|
|
1367
|
-
|
|
1818
|
+
if (patch.op === "setEdgeAnchors") return applySetEdgeAnchors(doc, patch);
|
|
1819
|
+
if (patch.op === "setNodePosition") return applySetNodePosition(doc, patch);
|
|
1820
|
+
if (patch.op === "removeNodePosition") return applyRemoveNodePosition(doc, patch);
|
|
1821
|
+
if (patch.op === "resetLayout") return applyResetLayout(doc, patch);
|
|
1822
|
+
if (patch.op === "addComment") return applyAddComment(doc, patch);
|
|
1823
|
+
if (patch.op === "updateComment") return applyUpdateComment(doc, patch);
|
|
1824
|
+
if (patch.op === "removeComment") return applyRemoveComment(doc, patch);
|
|
1368
1825
|
const nextSession = produce(doc.session, (d) => {
|
|
1369
1826
|
const draft = d;
|
|
1370
1827
|
switch (patch.op) {
|
|
@@ -1410,6 +1867,11 @@ function applyPatch(doc, patch) {
|
|
|
1410
1867
|
case "renameState": {
|
|
1411
1868
|
const wf = draft.workflows.find((w) => w.name === patch.workflow);
|
|
1412
1869
|
if (!wf) return;
|
|
1870
|
+
if (patch.to !== patch.from && patch.to in wf.states) {
|
|
1871
|
+
throw new PatchConflictError(
|
|
1872
|
+
`State "${patch.to}" already exists in workflow "${patch.workflow}"`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1413
1875
|
renameStateCascading(wf, patch.from, patch.to);
|
|
1414
1876
|
return;
|
|
1415
1877
|
}
|
|
@@ -1457,6 +1919,23 @@ function applyPatch(doc, patch) {
|
|
|
1457
1919
|
state.transitions.splice(patch.toIndex, 0, item);
|
|
1458
1920
|
return;
|
|
1459
1921
|
}
|
|
1922
|
+
case "moveTransitionSource": {
|
|
1923
|
+
const wf = draft.workflows.find((w) => w.name === patch.workflow);
|
|
1924
|
+
if (!wf) return;
|
|
1925
|
+
const fromState = wf.states[patch.fromState];
|
|
1926
|
+
const toState = wf.states[patch.toState];
|
|
1927
|
+
if (!fromState || !toState) return;
|
|
1928
|
+
const idx = fromState.transitions.findIndex((t) => t.name === patch.transitionName);
|
|
1929
|
+
if (idx < 0) return;
|
|
1930
|
+
if (patch.fromState !== patch.toState && toState.transitions.some((t) => t.name === patch.transitionName)) {
|
|
1931
|
+
throw new PatchConflictError(
|
|
1932
|
+
`Transition "${patch.transitionName}" already exists in state "${patch.toState}"`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
const [transition] = fromState.transitions.splice(idx, 1);
|
|
1936
|
+
if (transition) toState.transitions.push(transition);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1460
1939
|
case "addProcessor": {
|
|
1461
1940
|
const loc = locateTransition(doc, patch.transitionUuid);
|
|
1462
1941
|
if (!loc) return;
|
|
@@ -1539,9 +2018,11 @@ function applyPatch(doc, patch) {
|
|
|
1539
2018
|
}
|
|
1540
2019
|
});
|
|
1541
2020
|
const nextMeta = assignSyntheticIds(nextSession, doc.meta);
|
|
2021
|
+
preserveMovedTransitionUuid(doc, patch, nextSession, nextMeta);
|
|
2022
|
+
const cleanedWorkflowUi = cleanupWorkflowUi(nextMeta.workflowUi, nextSession, nextMeta);
|
|
1542
2023
|
return {
|
|
1543
2024
|
session: nextSession,
|
|
1544
|
-
meta: { ...nextMeta, revision: doc.meta.revision + 1 }
|
|
2025
|
+
meta: { ...nextMeta, workflowUi: cleanedWorkflowUi, revision: doc.meta.revision + 1 }
|
|
1545
2026
|
};
|
|
1546
2027
|
}
|
|
1547
2028
|
function applyPatches(doc, patches) {
|
|
@@ -1574,6 +2055,124 @@ function applySetEdgeAnchors(doc, patch) {
|
|
|
1574
2055
|
}
|
|
1575
2056
|
};
|
|
1576
2057
|
}
|
|
2058
|
+
function applySetNodePosition(doc, patch) {
|
|
2059
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2060
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2061
|
+
const nodes = { ...current.layout?.nodes ?? {} };
|
|
2062
|
+
nodes[patch.stateCode] = { x: patch.x, y: patch.y, pinned: patch.pinned ?? true };
|
|
2063
|
+
workflowUi[patch.workflow] = { ...current, layout: { nodes } };
|
|
2064
|
+
return {
|
|
2065
|
+
session: doc.session,
|
|
2066
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
function applyRemoveNodePosition(doc, patch) {
|
|
2070
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2071
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2072
|
+
const nodes = { ...current.layout?.nodes ?? {} };
|
|
2073
|
+
delete nodes[patch.stateCode];
|
|
2074
|
+
workflowUi[patch.workflow] = {
|
|
2075
|
+
...current,
|
|
2076
|
+
layout: Object.keys(nodes).length > 0 ? { nodes } : void 0
|
|
2077
|
+
};
|
|
2078
|
+
return {
|
|
2079
|
+
session: doc.session,
|
|
2080
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function applyResetLayout(doc, patch) {
|
|
2084
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2085
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2086
|
+
workflowUi[patch.workflow] = { ...current, layout: void 0 };
|
|
2087
|
+
return {
|
|
2088
|
+
session: doc.session,
|
|
2089
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function applyAddComment(doc, patch) {
|
|
2093
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2094
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2095
|
+
const comments = { ...current.comments ?? {}, [patch.comment.id]: patch.comment };
|
|
2096
|
+
workflowUi[patch.workflow] = { ...current, comments };
|
|
2097
|
+
return {
|
|
2098
|
+
session: doc.session,
|
|
2099
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
function applyUpdateComment(doc, patch) {
|
|
2103
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2104
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2105
|
+
const existing = current.comments?.[patch.commentId];
|
|
2106
|
+
if (!existing) return { ...doc, meta: { ...doc.meta, revision: doc.meta.revision + 1 } };
|
|
2107
|
+
const updated = { ...existing, ...patch.updates, id: existing.id };
|
|
2108
|
+
const comments = { ...current.comments ?? {}, [patch.commentId]: updated };
|
|
2109
|
+
workflowUi[patch.workflow] = { ...current, comments };
|
|
2110
|
+
return {
|
|
2111
|
+
session: doc.session,
|
|
2112
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function applyRemoveComment(doc, patch) {
|
|
2116
|
+
const workflowUi = { ...doc.meta.workflowUi };
|
|
2117
|
+
const current = workflowUi[patch.workflow] ?? {};
|
|
2118
|
+
const comments = { ...current.comments ?? {} };
|
|
2119
|
+
delete comments[patch.commentId];
|
|
2120
|
+
workflowUi[patch.workflow] = {
|
|
2121
|
+
...current,
|
|
2122
|
+
comments: Object.keys(comments).length > 0 ? comments : void 0
|
|
2123
|
+
};
|
|
2124
|
+
return {
|
|
2125
|
+
session: doc.session,
|
|
2126
|
+
meta: { ...doc.meta, workflowUi, revision: doc.meta.revision + 1 }
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
function preserveMovedTransitionUuid(priorDoc, patch, nextSession, nextMeta) {
|
|
2130
|
+
if (patch.op !== "moveTransitionSource") return;
|
|
2131
|
+
const oldUuid = transitionUuidByName(
|
|
2132
|
+
priorDoc,
|
|
2133
|
+
patch.workflow,
|
|
2134
|
+
patch.fromState,
|
|
2135
|
+
patch.transitionName
|
|
2136
|
+
);
|
|
2137
|
+
if (!oldUuid) return;
|
|
2138
|
+
const newUuid = transitionUuidByNameInSession(
|
|
2139
|
+
nextSession,
|
|
2140
|
+
nextMeta,
|
|
2141
|
+
patch.workflow,
|
|
2142
|
+
patch.toState,
|
|
2143
|
+
patch.transitionName
|
|
2144
|
+
);
|
|
2145
|
+
if (!newUuid || newUuid === oldUuid) return;
|
|
2146
|
+
const newPtr = nextMeta.ids.transitions[newUuid];
|
|
2147
|
+
if (!newPtr) return;
|
|
2148
|
+
delete nextMeta.ids.transitions[newUuid];
|
|
2149
|
+
nextMeta.ids.transitions[oldUuid] = {
|
|
2150
|
+
...newPtr,
|
|
2151
|
+
transitionUuid: oldUuid
|
|
2152
|
+
};
|
|
2153
|
+
for (const processorPtr of Object.values(nextMeta.ids.processors)) {
|
|
2154
|
+
if (processorPtr.transitionUuid === newUuid) {
|
|
2155
|
+
processorPtr.transitionUuid = oldUuid;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
for (const criterionPtr of Object.values(nextMeta.ids.criteria)) {
|
|
2159
|
+
const host = criterionPtr.host;
|
|
2160
|
+
if ((host.kind === "transition" || host.kind === "processorConfig") && host.transitionUuid === newUuid) {
|
|
2161
|
+
host.transitionUuid = oldUuid;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function transitionUuidByName(doc, workflow, state, transitionName) {
|
|
2166
|
+
return transitionUuidByNameInSession(doc.session, doc.meta, workflow, state, transitionName);
|
|
2167
|
+
}
|
|
2168
|
+
function transitionUuidByNameInSession(session, meta, workflow, state, transitionName) {
|
|
2169
|
+
const wf = session.workflows.find((candidate) => candidate.name === workflow);
|
|
2170
|
+
const transitions = wf?.states[state]?.transitions ?? [];
|
|
2171
|
+
const index = transitions.findIndex((transition) => transition.name === transitionName);
|
|
2172
|
+
if (index < 0) return null;
|
|
2173
|
+
const ordered = Object.entries(meta.ids.transitions).filter(([, ptr]) => ptr.workflow === workflow && ptr.state === state).map(([uuid]) => uuid);
|
|
2174
|
+
return ordered[index] ?? null;
|
|
2175
|
+
}
|
|
1577
2176
|
function renameStateCascading(wf, from, to) {
|
|
1578
2177
|
if (!(from in wf.states) || from === to) return;
|
|
1579
2178
|
wf.states[to] = wf.states[from];
|
|
@@ -1622,6 +2221,65 @@ function locateProcessor(doc, processorUuid) {
|
|
|
1622
2221
|
processorIndex: pIdx
|
|
1623
2222
|
};
|
|
1624
2223
|
}
|
|
2224
|
+
function cleanupWorkflowUi(workflowUi, session, meta) {
|
|
2225
|
+
const wfNames = new Set(session.workflows.map((w) => w.name));
|
|
2226
|
+
const result = {};
|
|
2227
|
+
const validTransitionIdsByWorkflow = /* @__PURE__ */ new Map();
|
|
2228
|
+
if (meta) {
|
|
2229
|
+
for (const [uuid, ptr] of Object.entries(meta.ids.transitions)) {
|
|
2230
|
+
let set = validTransitionIdsByWorkflow.get(ptr.workflow);
|
|
2231
|
+
if (!set) {
|
|
2232
|
+
set = /* @__PURE__ */ new Set();
|
|
2233
|
+
validTransitionIdsByWorkflow.set(ptr.workflow, set);
|
|
2234
|
+
}
|
|
2235
|
+
set.add(uuid);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
for (const [wfName, ui] of Object.entries(workflowUi)) {
|
|
2239
|
+
if (!wfNames.has(wfName)) continue;
|
|
2240
|
+
const wf = session.workflows.find((w) => w.name === wfName);
|
|
2241
|
+
const existingStates = wf ? new Set(Object.keys(wf.states)) : /* @__PURE__ */ new Set();
|
|
2242
|
+
const allTransitionNames = /* @__PURE__ */ new Set();
|
|
2243
|
+
if (wf) {
|
|
2244
|
+
for (const state of Object.values(wf.states)) {
|
|
2245
|
+
for (const t of state.transitions) allTransitionNames.add(t.name);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
let layout = ui.layout;
|
|
2249
|
+
if (layout?.nodes) {
|
|
2250
|
+
const cleanNodes = Object.fromEntries(
|
|
2251
|
+
Object.entries(layout.nodes).filter(([code]) => existingStates.has(code))
|
|
2252
|
+
);
|
|
2253
|
+
layout = Object.keys(cleanNodes).length > 0 ? { nodes: cleanNodes } : void 0;
|
|
2254
|
+
}
|
|
2255
|
+
let comments = ui.comments;
|
|
2256
|
+
if (comments) {
|
|
2257
|
+
const cleanComments = {};
|
|
2258
|
+
for (const [id, c] of Object.entries(comments)) {
|
|
2259
|
+
if (c.attachedTo?.kind === "state" && !existingStates.has(c.attachedTo.stateCode)) {
|
|
2260
|
+
cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
|
|
2261
|
+
} else if (c.attachedTo?.kind === "transition" && !allTransitionNames.has(c.attachedTo.transitionName)) {
|
|
2262
|
+
cleanComments[id] = { ...c, attachedTo: { kind: "free" } };
|
|
2263
|
+
} else {
|
|
2264
|
+
cleanComments[id] = c;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
comments = Object.keys(cleanComments).length > 0 ? cleanComments : void 0;
|
|
2268
|
+
}
|
|
2269
|
+
let edgeAnchors = ui.edgeAnchors;
|
|
2270
|
+
if (edgeAnchors && meta) {
|
|
2271
|
+
const validTransitionIds = validTransitionIdsByWorkflow.get(wfName) ?? /* @__PURE__ */ new Set();
|
|
2272
|
+
const cleanAnchors = Object.fromEntries(
|
|
2273
|
+
Object.entries(edgeAnchors).filter(
|
|
2274
|
+
([transitionUuid]) => validTransitionIds.has(transitionUuid)
|
|
2275
|
+
)
|
|
2276
|
+
);
|
|
2277
|
+
edgeAnchors = Object.keys(cleanAnchors).length > 0 ? cleanAnchors : void 0;
|
|
2278
|
+
}
|
|
2279
|
+
result[wfName] = { ...ui, layout, comments, edgeAnchors };
|
|
2280
|
+
}
|
|
2281
|
+
return result;
|
|
2282
|
+
}
|
|
1625
2283
|
function applyCriterionAtPath(container, path, criterion) {
|
|
1626
2284
|
if (path.length === 0) return;
|
|
1627
2285
|
let node = container;
|
|
@@ -1658,7 +2316,6 @@ function invertPatch(doc, patch) {
|
|
|
1658
2316
|
case "removeWorkflow":
|
|
1659
2317
|
case "renameWorkflow":
|
|
1660
2318
|
case "removeState":
|
|
1661
|
-
case "renameState":
|
|
1662
2319
|
case "replaceSession":
|
|
1663
2320
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1664
2321
|
case "updateWorkflowMeta": {
|
|
@@ -1673,30 +2330,19 @@ function invertPatch(doc, patch) {
|
|
|
1673
2330
|
case "setInitialState": {
|
|
1674
2331
|
const wf = findWorkflow(doc, patch.workflow);
|
|
1675
2332
|
if (!wf) return noop();
|
|
1676
|
-
return {
|
|
1677
|
-
op: "setInitialState",
|
|
1678
|
-
workflow: patch.workflow,
|
|
1679
|
-
stateCode: wf.initialState
|
|
1680
|
-
};
|
|
2333
|
+
return { op: "setInitialState", workflow: patch.workflow, stateCode: wf.initialState };
|
|
1681
2334
|
}
|
|
1682
2335
|
case "setWorkflowCriterion": {
|
|
1683
2336
|
const wf = findWorkflow(doc, patch.workflow);
|
|
1684
2337
|
if (!wf) return noop();
|
|
1685
|
-
return wf.criterion ? {
|
|
1686
|
-
op: "setWorkflowCriterion",
|
|
1687
|
-
workflow: patch.workflow,
|
|
1688
|
-
criterion: cloneCriterion(wf.criterion)
|
|
1689
|
-
} : { op: "setWorkflowCriterion", workflow: patch.workflow };
|
|
2338
|
+
return wf.criterion ? { op: "setWorkflowCriterion", workflow: patch.workflow, criterion: cloneCriterion(wf.criterion) } : { op: "setWorkflowCriterion", workflow: patch.workflow };
|
|
1690
2339
|
}
|
|
1691
2340
|
case "addState":
|
|
1692
|
-
return {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
};
|
|
1697
|
-
case "addTransition": {
|
|
2341
|
+
return { op: "removeState", workflow: patch.workflow, stateCode: patch.stateCode };
|
|
2342
|
+
case "renameState":
|
|
2343
|
+
return { op: "renameState", workflow: patch.workflow, from: patch.to, to: patch.from };
|
|
2344
|
+
case "addTransition":
|
|
1698
2345
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1699
|
-
}
|
|
1700
2346
|
case "updateTransition": {
|
|
1701
2347
|
const t = findTransition(doc, patch.transitionUuid);
|
|
1702
2348
|
if (!t) return noop();
|
|
@@ -1704,15 +2350,48 @@ function invertPatch(doc, patch) {
|
|
|
1704
2350
|
for (const key of Object.keys(patch.updates)) {
|
|
1705
2351
|
prior[key] = t[key];
|
|
1706
2352
|
}
|
|
2353
|
+
return { op: "updateTransition", transitionUuid: patch.transitionUuid, updates: prior };
|
|
2354
|
+
}
|
|
2355
|
+
case "removeTransition": {
|
|
2356
|
+
const loc = locateTransition2(doc, patch.transitionUuid);
|
|
2357
|
+
if (!loc) return noop();
|
|
2358
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2359
|
+
const state = wf?.states[loc.state];
|
|
2360
|
+
const t = state?.transitions[loc.index];
|
|
2361
|
+
if (!t) return noop();
|
|
1707
2362
|
return {
|
|
1708
|
-
op: "
|
|
1709
|
-
|
|
1710
|
-
|
|
2363
|
+
op: "addTransition",
|
|
2364
|
+
workflow: loc.workflow,
|
|
2365
|
+
fromState: loc.state,
|
|
2366
|
+
transition: structuredClone(t)
|
|
1711
2367
|
};
|
|
1712
2368
|
}
|
|
1713
|
-
case "
|
|
1714
|
-
|
|
1715
|
-
|
|
2369
|
+
case "reorderTransition": {
|
|
2370
|
+
const loc = locateTransition2(doc, patch.transitionUuid);
|
|
2371
|
+
if (!loc) return noop();
|
|
2372
|
+
const orderedForState = [];
|
|
2373
|
+
for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
|
|
2374
|
+
if (p.workflow === patch.workflow && p.state === patch.fromState) {
|
|
2375
|
+
orderedForState.push(uuid);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
const uuidAtTarget = orderedForState[patch.toIndex] ?? patch.transitionUuid;
|
|
2379
|
+
return {
|
|
2380
|
+
op: "reorderTransition",
|
|
2381
|
+
workflow: patch.workflow,
|
|
2382
|
+
fromState: patch.fromState,
|
|
2383
|
+
transitionUuid: uuidAtTarget,
|
|
2384
|
+
toIndex: loc.index
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
case "moveTransitionSource":
|
|
2388
|
+
return {
|
|
2389
|
+
op: "moveTransitionSource",
|
|
2390
|
+
workflow: patch.workflow,
|
|
2391
|
+
fromState: patch.toState,
|
|
2392
|
+
toState: patch.fromState,
|
|
2393
|
+
transitionName: patch.transitionName
|
|
2394
|
+
};
|
|
1716
2395
|
case "addProcessor":
|
|
1717
2396
|
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1718
2397
|
case "updateProcessor": {
|
|
@@ -1722,23 +2401,43 @@ function invertPatch(doc, patch) {
|
|
|
1722
2401
|
for (const key of Object.keys(patch.updates)) {
|
|
1723
2402
|
prior[key] = p[key];
|
|
1724
2403
|
}
|
|
2404
|
+
return { op: "updateProcessor", processorUuid: patch.processorUuid, updates: prior };
|
|
2405
|
+
}
|
|
2406
|
+
case "removeProcessor": {
|
|
2407
|
+
const ptr = doc.meta.ids.processors[patch.processorUuid];
|
|
2408
|
+
if (!ptr) return noop();
|
|
2409
|
+
const procLoc = locateProcessor2(doc, patch.processorUuid);
|
|
2410
|
+
if (!procLoc) return noop();
|
|
2411
|
+
const wf = findWorkflow(doc, procLoc.workflow);
|
|
2412
|
+
const state = wf?.states[procLoc.state];
|
|
2413
|
+
const t = state?.transitions[procLoc.transitionIndex];
|
|
2414
|
+
const p = t?.processors?.[procLoc.processorIndex];
|
|
2415
|
+
if (!p) return noop();
|
|
1725
2416
|
return {
|
|
1726
|
-
op: "
|
|
1727
|
-
|
|
1728
|
-
|
|
2417
|
+
op: "addProcessor",
|
|
2418
|
+
transitionUuid: ptr.transitionUuid,
|
|
2419
|
+
processor: structuredClone(p),
|
|
2420
|
+
index: procLoc.processorIndex
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
case "reorderProcessor": {
|
|
2424
|
+
const procLoc = locateProcessor2(doc, patch.processorUuid);
|
|
2425
|
+
if (!procLoc) return noop();
|
|
2426
|
+
const orderedForTransition = [];
|
|
2427
|
+
for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
|
|
2428
|
+
if (p.transitionUuid === patch.transitionUuid) orderedForTransition.push(uuid);
|
|
2429
|
+
}
|
|
2430
|
+
const uuidAtTarget = orderedForTransition[patch.toIndex] ?? patch.processorUuid;
|
|
2431
|
+
return {
|
|
2432
|
+
op: "reorderProcessor",
|
|
2433
|
+
transitionUuid: patch.transitionUuid,
|
|
2434
|
+
processorUuid: uuidAtTarget,
|
|
2435
|
+
toIndex: procLoc.processorIndex
|
|
1729
2436
|
};
|
|
1730
2437
|
}
|
|
1731
|
-
case "removeProcessor":
|
|
1732
|
-
case "reorderProcessor":
|
|
1733
|
-
return { op: "replaceSession", session: cloneSession(doc) };
|
|
1734
2438
|
case "setCriterion": {
|
|
1735
2439
|
const prior = readCriterionAt(doc, patch.host, patch.path);
|
|
1736
|
-
return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : {
|
|
1737
|
-
op: "setCriterion",
|
|
1738
|
-
host: patch.host,
|
|
1739
|
-
path: patch.path,
|
|
1740
|
-
criterion: cloneCriterion(prior)
|
|
1741
|
-
};
|
|
2440
|
+
return prior === void 0 ? { op: "setCriterion", host: patch.host, path: patch.path } : { op: "setCriterion", host: patch.host, path: patch.path, criterion: cloneCriterion(prior) };
|
|
1742
2441
|
}
|
|
1743
2442
|
case "setImportMode":
|
|
1744
2443
|
return { op: "setImportMode", mode: doc.session.importMode };
|
|
@@ -1754,6 +2453,36 @@ function invertPatch(doc, patch) {
|
|
|
1754
2453
|
anchors: prior ? { ...prior } : null
|
|
1755
2454
|
};
|
|
1756
2455
|
}
|
|
2456
|
+
case "setNodePosition": {
|
|
2457
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
|
|
2458
|
+
if (!prior) {
|
|
2459
|
+
return { op: "removeNodePosition", workflow: patch.workflow, stateCode: patch.stateCode };
|
|
2460
|
+
}
|
|
2461
|
+
return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
|
|
2462
|
+
}
|
|
2463
|
+
case "removeNodePosition": {
|
|
2464
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.layout?.nodes?.[patch.stateCode];
|
|
2465
|
+
if (!prior) return noop();
|
|
2466
|
+
return { op: "setNodePosition", workflow: patch.workflow, stateCode: patch.stateCode, ...prior };
|
|
2467
|
+
}
|
|
2468
|
+
case "resetLayout":
|
|
2469
|
+
return noop();
|
|
2470
|
+
case "addComment":
|
|
2471
|
+
return { op: "removeComment", workflow: patch.workflow, commentId: patch.comment.id };
|
|
2472
|
+
case "updateComment": {
|
|
2473
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
|
|
2474
|
+
if (!prior) return noop();
|
|
2475
|
+
const priorUpdates = {};
|
|
2476
|
+
for (const key of Object.keys(patch.updates)) {
|
|
2477
|
+
priorUpdates[key] = prior[key];
|
|
2478
|
+
}
|
|
2479
|
+
return { op: "updateComment", workflow: patch.workflow, commentId: patch.commentId, updates: priorUpdates };
|
|
2480
|
+
}
|
|
2481
|
+
case "removeComment": {
|
|
2482
|
+
const prior = doc.meta.workflowUi[patch.workflow]?.comments?.[patch.commentId];
|
|
2483
|
+
if (!prior) return noop();
|
|
2484
|
+
return { op: "addComment", workflow: patch.workflow, comment: structuredClone(prior) };
|
|
2485
|
+
}
|
|
1757
2486
|
}
|
|
1758
2487
|
}
|
|
1759
2488
|
function noop() {
|
|
@@ -1762,30 +2491,42 @@ function noop() {
|
|
|
1762
2491
|
function findWorkflow(doc, name) {
|
|
1763
2492
|
return doc.session.workflows.find((w) => w.name === name);
|
|
1764
2493
|
}
|
|
1765
|
-
function
|
|
2494
|
+
function locateTransition2(doc, transitionUuid) {
|
|
1766
2495
|
const ptr = doc.meta.ids.transitions[transitionUuid];
|
|
1767
|
-
if (!ptr) return
|
|
1768
|
-
const wf = findWorkflow(doc, ptr.workflow);
|
|
1769
|
-
const state = wf?.states[ptr.state];
|
|
1770
|
-
if (!state) return void 0;
|
|
2496
|
+
if (!ptr) return null;
|
|
1771
2497
|
const ordered = [];
|
|
1772
2498
|
for (const [uuid, p] of Object.entries(doc.meta.ids.transitions)) {
|
|
1773
2499
|
if (p.workflow === ptr.workflow && p.state === ptr.state) ordered.push(uuid);
|
|
1774
2500
|
}
|
|
1775
2501
|
const idx = ordered.indexOf(transitionUuid);
|
|
1776
|
-
return
|
|
2502
|
+
if (idx < 0) return null;
|
|
2503
|
+
return { workflow: ptr.workflow, state: ptr.state, index: idx };
|
|
1777
2504
|
}
|
|
1778
|
-
function
|
|
2505
|
+
function locateProcessor2(doc, processorUuid) {
|
|
1779
2506
|
const ptr = doc.meta.ids.processors[processorUuid];
|
|
1780
|
-
if (!ptr) return
|
|
1781
|
-
const
|
|
1782
|
-
if (!
|
|
2507
|
+
if (!ptr) return null;
|
|
2508
|
+
const tLoc = locateTransition2(doc, ptr.transitionUuid);
|
|
2509
|
+
if (!tLoc) return null;
|
|
1783
2510
|
const ordered = [];
|
|
1784
2511
|
for (const [uuid, p] of Object.entries(doc.meta.ids.processors)) {
|
|
1785
2512
|
if (p.transitionUuid === ptr.transitionUuid) ordered.push(uuid);
|
|
1786
2513
|
}
|
|
1787
|
-
const
|
|
1788
|
-
return
|
|
2514
|
+
const pIdx = ordered.indexOf(processorUuid);
|
|
2515
|
+
if (pIdx < 0) return null;
|
|
2516
|
+
return { workflow: tLoc.workflow, state: tLoc.state, transitionIndex: tLoc.index, processorIndex: pIdx };
|
|
2517
|
+
}
|
|
2518
|
+
function findTransition(doc, transitionUuid) {
|
|
2519
|
+
const loc = locateTransition2(doc, transitionUuid);
|
|
2520
|
+
if (!loc) return void 0;
|
|
2521
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2522
|
+
return wf?.states[loc.state]?.transitions[loc.index];
|
|
2523
|
+
}
|
|
2524
|
+
function findProcessor(doc, processorUuid) {
|
|
2525
|
+
const loc = locateProcessor2(doc, processorUuid);
|
|
2526
|
+
if (!loc) return void 0;
|
|
2527
|
+
const wf = findWorkflow(doc, loc.workflow);
|
|
2528
|
+
const t = wf?.states[loc.state]?.transitions[loc.transitionIndex];
|
|
2529
|
+
return t?.processors?.[loc.processorIndex];
|
|
1789
2530
|
}
|
|
1790
2531
|
function readCriterionAt(doc, host, path) {
|
|
1791
2532
|
const wf = findWorkflow(doc, host.workflow);
|
|
@@ -1820,6 +2561,26 @@ function cloneCriterion(c) {
|
|
|
1820
2561
|
return structuredClone(c);
|
|
1821
2562
|
}
|
|
1822
2563
|
|
|
2564
|
+
// src/patch/transaction.ts
|
|
2565
|
+
function applyTransaction(doc, tx) {
|
|
2566
|
+
return tx.patches.reduce((d, p) => applyPatch(d, p), doc);
|
|
2567
|
+
}
|
|
2568
|
+
function invertTransaction(doc, tx) {
|
|
2569
|
+
if (tx.inverses.length > 0) {
|
|
2570
|
+
return {
|
|
2571
|
+
summary: `Undo: ${tx.summary}`,
|
|
2572
|
+
patches: tx.inverses,
|
|
2573
|
+
inverses: tx.patches
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
const computed = tx.patches.map((p) => invertPatch(doc, p)).reverse();
|
|
2577
|
+
return {
|
|
2578
|
+
summary: `Undo: ${tx.summary}`,
|
|
2579
|
+
patches: computed,
|
|
2580
|
+
inverses: tx.patches
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
1823
2584
|
// src/migrate/registry.ts
|
|
1824
2585
|
var registry = [];
|
|
1825
2586
|
function registerMigration(entry) {
|
|
@@ -1857,8 +2618,59 @@ function migrateSession(session, from, to) {
|
|
|
1857
2618
|
return path.reduce((s, entry) => entry.migrate(s), session);
|
|
1858
2619
|
}
|
|
1859
2620
|
registerMigration({ from: "1.0", to: "1.0", migrate: (s) => s });
|
|
2621
|
+
|
|
2622
|
+
// src/criteria/describe.ts
|
|
2623
|
+
var OPERATOR_SYMBOL = {
|
|
2624
|
+
EQUALS: "=",
|
|
2625
|
+
NOT_EQUAL: "\u2260",
|
|
2626
|
+
GREATER_THAN: ">",
|
|
2627
|
+
LESS_THAN: "<",
|
|
2628
|
+
GREATER_OR_EQUAL: "\u2265",
|
|
2629
|
+
LESS_OR_EQUAL: "\u2264",
|
|
2630
|
+
IEQUALS: "=",
|
|
2631
|
+
INOT_EQUAL: "\u2260"
|
|
2632
|
+
};
|
|
2633
|
+
function describeCriterion(c) {
|
|
2634
|
+
switch (c.type) {
|
|
2635
|
+
case "simple":
|
|
2636
|
+
return describeBinary(c.jsonPath, c.operation, c.value);
|
|
2637
|
+
case "group": {
|
|
2638
|
+
const n = c.conditions.length;
|
|
2639
|
+
return `${c.operator} (${n} condition${n === 1 ? "" : "s"})`;
|
|
2640
|
+
}
|
|
2641
|
+
case "function":
|
|
2642
|
+
return `Function: ${c.function.name || "<unnamed>"}`;
|
|
2643
|
+
case "lifecycle":
|
|
2644
|
+
return describeBinary(c.field, c.operation, c.value);
|
|
2645
|
+
case "array": {
|
|
2646
|
+
const n = c.value.length;
|
|
2647
|
+
return `${c.jsonPath} ${c.operation} [${n} value${n === 1 ? "" : "s"}]`;
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
function describeBinary(lhs, op, value) {
|
|
2652
|
+
if (op === "IS_NULL") return `${lhs} IS NULL`;
|
|
2653
|
+
if (op === "NOT_NULL") return `${lhs} IS NOT NULL`;
|
|
2654
|
+
if (op === "IS_CHANGED") return `${lhs} CHANGED`;
|
|
2655
|
+
if (op === "IS_UNCHANGED") return `${lhs} UNCHANGED`;
|
|
2656
|
+
if (op === "BETWEEN" || op === "BETWEEN_INCLUSIVE") {
|
|
2657
|
+
const inclusive = op === "BETWEEN_INCLUSIVE";
|
|
2658
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
2659
|
+
return `${lhs} \u2208 ${inclusive ? "[" : "("}${formatValue(value[0])}, ${formatValue(value[1])}${inclusive ? "]" : ")"}`;
|
|
2660
|
+
}
|
|
2661
|
+
return `${lhs} ${op} ${formatValue(value)}`;
|
|
2662
|
+
}
|
|
2663
|
+
const symbol = OPERATOR_SYMBOL[op] ?? op;
|
|
2664
|
+
return `${lhs} ${symbol} ${formatValue(value)}`;
|
|
2665
|
+
}
|
|
2666
|
+
function formatValue(v) {
|
|
2667
|
+
if (v === void 0) return "?";
|
|
2668
|
+
if (typeof v === "string") return JSON.stringify(v);
|
|
2669
|
+
return JSON.stringify(v);
|
|
2670
|
+
}
|
|
1860
2671
|
export {
|
|
1861
2672
|
ArrayCriterionSchema,
|
|
2673
|
+
CRITERION_DEPTH_WARNING_THRESHOLD,
|
|
1862
2674
|
CriterionSchema,
|
|
1863
2675
|
ExecutionModeSchema,
|
|
1864
2676
|
ExportPayloadSchema,
|
|
@@ -1868,26 +2680,38 @@ export {
|
|
|
1868
2680
|
GroupCriterionSchema,
|
|
1869
2681
|
ImportPayloadSchema,
|
|
1870
2682
|
LifecycleCriterionSchema,
|
|
2683
|
+
MAX_CRITERION_DEPTH,
|
|
2684
|
+
MAX_JSON_BYTES,
|
|
2685
|
+
MAX_JSON_OBJECT_DEPTH,
|
|
1871
2686
|
NAME_REGEX,
|
|
1872
2687
|
NameSchema,
|
|
2688
|
+
OPERATOR_GROUPS,
|
|
1873
2689
|
OPERATOR_TYPES,
|
|
2690
|
+
OPERATOR_VALUE_SHAPE,
|
|
1874
2691
|
OperatorEnum,
|
|
1875
2692
|
ParseJsonError,
|
|
2693
|
+
PatchConflictError,
|
|
1876
2694
|
ProcessorSchema,
|
|
2695
|
+
SUPPORTED_GROUP_OPERATORS,
|
|
2696
|
+
SUPPORTED_SIMPLE_OPERATORS,
|
|
1877
2697
|
ScheduledProcessorSchema,
|
|
1878
2698
|
SchemaError,
|
|
1879
2699
|
SimpleCriterionSchema,
|
|
1880
2700
|
StateSchema,
|
|
1881
2701
|
TransitionSchema,
|
|
2702
|
+
UNSUPPORTED_OPERATORS,
|
|
1882
2703
|
WorkflowApiConflictError,
|
|
1883
2704
|
WorkflowApiTransportError,
|
|
1884
2705
|
WorkflowSchema,
|
|
1885
2706
|
applyPatch,
|
|
1886
2707
|
applyPatches,
|
|
2708
|
+
applyTransaction,
|
|
1887
2709
|
assignSyntheticIds,
|
|
2710
|
+
describeCriterion,
|
|
1888
2711
|
findMigrationPath,
|
|
1889
|
-
|
|
2712
|
+
idFor,
|
|
1890
2713
|
invertPatch,
|
|
2714
|
+
invertTransaction,
|
|
1891
2715
|
listMigrations,
|
|
1892
2716
|
lookupById,
|
|
1893
2717
|
migrateSession,
|
|
@@ -1913,6 +2737,7 @@ export {
|
|
|
1913
2737
|
validateAll,
|
|
1914
2738
|
validateExportSchema,
|
|
1915
2739
|
validateImportSchema,
|
|
2740
|
+
validateJsonPathSubset,
|
|
1916
2741
|
validateSemantics,
|
|
1917
2742
|
validateSession,
|
|
1918
2743
|
zodErrorToIssues
|