@sanity/workflow-engine 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks-cjs/schema.cjs +10 -0
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +10 -0
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +1 -0
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +134 -0
- package/dist/define.d.ts +134 -0
- package/dist/define.js +1 -0
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +355 -222
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +391 -77
- package/dist/index.d.ts +391 -77
- package/dist/index.js +355 -222
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -95,6 +95,21 @@ function gdrFromResource(res, documentId) {
|
|
|
95
95
|
}
|
|
96
96
|
return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
|
|
97
97
|
}
|
|
98
|
+
function resourceFromGdrUri(uri) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseGdr(uri);
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (parsed.scheme === "dataset")
|
|
106
|
+
return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
|
|
107
|
+
if (parsed.resourceId !== void 0)
|
|
108
|
+
return { type: parsed.scheme, id: parsed.resourceId };
|
|
109
|
+
}
|
|
110
|
+
function sameResource(a, b) {
|
|
111
|
+
return a.type === b.type && a.id === b.id;
|
|
112
|
+
}
|
|
98
113
|
function selfGdr(doc) {
|
|
99
114
|
return gdrFromResource(doc.workflowResource, doc._id);
|
|
100
115
|
}
|
|
@@ -239,6 +254,8 @@ function validateTask(v2, stageName, task) {
|
|
|
239
254
|
for (const entry of task.state ?? [])
|
|
240
255
|
v2.checkEntry(entry, `${where}.state "${entry.name}"`);
|
|
241
256
|
task.filter !== void 0 && v2.checkCondition(task.filter, `${where}.filter`), task.completeWhen !== void 0 && v2.checkCondition(task.completeWhen, `${where}.completeWhen`), task.failWhen !== void 0 && v2.checkCondition(task.failWhen, `${where}.failWhen`);
|
|
257
|
+
for (const [name, groq] of Object.entries(task.requirements ?? {}))
|
|
258
|
+
v2.checkCondition(groq, `${where}.requirements "${name}"`);
|
|
242
259
|
for (const [key, groq] of effectBindings(task.effects))
|
|
243
260
|
v2.tryParse(groq, `${where} effect binding "${key}"`);
|
|
244
261
|
validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
|
|
@@ -476,10 +493,14 @@ function abortReason(instance) {
|
|
|
476
493
|
)?.reason;
|
|
477
494
|
}
|
|
478
495
|
function diagnoseInputFromEvaluation(evaluation) {
|
|
496
|
+
const stage = openStage(evaluation.instance);
|
|
479
497
|
return {
|
|
480
498
|
instance: evaluation.instance,
|
|
481
499
|
tasks: evaluation.currentStage.tasks,
|
|
482
|
-
transitions: evaluation.currentStage.transitions
|
|
500
|
+
transitions: evaluation.currentStage.transitions,
|
|
501
|
+
assignees: Object.fromEntries(
|
|
502
|
+
evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
|
|
503
|
+
)
|
|
483
504
|
};
|
|
484
505
|
}
|
|
485
506
|
function openStage(instance) {
|
|
@@ -504,18 +525,34 @@ function failedTaskCause(input) {
|
|
|
504
525
|
return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
|
|
505
526
|
}
|
|
506
527
|
function waitingState(input) {
|
|
507
|
-
const active = input.tasks.find(
|
|
528
|
+
const active = input.tasks.find(
|
|
529
|
+
(t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
|
|
530
|
+
);
|
|
508
531
|
if (active !== void 0)
|
|
509
532
|
return {
|
|
510
533
|
state: "waiting",
|
|
511
534
|
task: active.task.name,
|
|
512
|
-
assignees:
|
|
535
|
+
assignees: input.assignees[active.task.name] ?? [],
|
|
513
536
|
actions: (active.task.actions ?? []).map((a) => a.name)
|
|
514
537
|
};
|
|
515
538
|
}
|
|
539
|
+
function blockedState(input) {
|
|
540
|
+
const blocked = input.tasks.find(
|
|
541
|
+
(t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
|
|
542
|
+
);
|
|
543
|
+
if (blocked !== void 0)
|
|
544
|
+
return {
|
|
545
|
+
state: "blocked",
|
|
546
|
+
task: blocked.task.name,
|
|
547
|
+
requirements: blocked.unmetRequirements ?? [],
|
|
548
|
+
assignees: input.assignees[blocked.task.name] ?? []
|
|
549
|
+
};
|
|
550
|
+
}
|
|
516
551
|
function noTransitionFiresCause(input) {
|
|
517
|
-
if (input.transitions.length
|
|
518
|
-
return
|
|
552
|
+
if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
|
|
553
|
+
return;
|
|
554
|
+
const undecidable = input.transitions.filter((t) => t.unevaluable);
|
|
555
|
+
return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
|
|
519
556
|
}
|
|
520
557
|
function diagnoseInstance(input) {
|
|
521
558
|
const { instance } = input;
|
|
@@ -526,7 +563,51 @@ function diagnoseInstance(input) {
|
|
|
526
563
|
if (instance.completedAt !== void 0)
|
|
527
564
|
return { state: "completed", at: instance.completedAt };
|
|
528
565
|
const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
|
|
529
|
-
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
|
|
566
|
+
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
|
|
567
|
+
}
|
|
568
|
+
const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
|
|
569
|
+
function remediationsForCause(cause) {
|
|
570
|
+
switch (cause.kind) {
|
|
571
|
+
case "failed-effect":
|
|
572
|
+
return [
|
|
573
|
+
{
|
|
574
|
+
verb: "retry-effect",
|
|
575
|
+
rationale: "Re-run the failed effect once the upstream system is healthy."
|
|
576
|
+
},
|
|
577
|
+
{ verb: "abort", rationale: "Abort the instance if it can no longer recover." }
|
|
578
|
+
];
|
|
579
|
+
case "hung-effect":
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
verb: "drain-effects",
|
|
583
|
+
rationale: "Re-run the effect drainer to re-pick the claimed effect."
|
|
584
|
+
},
|
|
585
|
+
{ verb: "abort", rationale: "Abort the instance if the effect never drains." }
|
|
586
|
+
];
|
|
587
|
+
case "failed-task":
|
|
588
|
+
return [
|
|
589
|
+
{ verb: "reset-task", rationale: "Reset or skip the failed task." },
|
|
590
|
+
{
|
|
591
|
+
verb: "set-stage",
|
|
592
|
+
rationale: "Move the instance past the failed task to the intended next stage."
|
|
593
|
+
}
|
|
594
|
+
];
|
|
595
|
+
case "no-transition-fires":
|
|
596
|
+
return [
|
|
597
|
+
{
|
|
598
|
+
verb: "set-stage",
|
|
599
|
+
rationale: "Move the instance to the intended next stage to force it forward."
|
|
600
|
+
}
|
|
601
|
+
];
|
|
602
|
+
case "transition-unevaluable":
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function remediationsFor(diagnosis) {
|
|
607
|
+
return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
|
|
608
|
+
...seed,
|
|
609
|
+
available: RUNNABLE_VERBS.has(seed.verb)
|
|
610
|
+
}));
|
|
530
611
|
}
|
|
531
612
|
function buildSnapshot(args) {
|
|
532
613
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
@@ -573,6 +654,7 @@ function collectWatchRefs(instance) {
|
|
|
573
654
|
function readsRaw(ref) {
|
|
574
655
|
return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
|
|
575
656
|
}
|
|
657
|
+
const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
|
|
576
658
|
function contentReleaseName(args) {
|
|
577
659
|
const { ref, perspective } = args;
|
|
578
660
|
if (!readsRaw(ref) && Array.isArray(perspective))
|
|
@@ -598,6 +680,11 @@ function entryReleaseRefs(state) {
|
|
|
598
680
|
(entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
|
|
599
681
|
);
|
|
600
682
|
}
|
|
683
|
+
function entrySubjectRefs(state) {
|
|
684
|
+
return stateEntries(state).flatMap(
|
|
685
|
+
(entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
|
|
686
|
+
);
|
|
687
|
+
}
|
|
601
688
|
function stateEntries(state) {
|
|
602
689
|
return Array.isArray(state) ? state.filter(
|
|
603
690
|
(entry) => !!entry && typeof entry == "object"
|
|
@@ -622,7 +709,10 @@ async function hydrateSnapshot(args) {
|
|
|
622
709
|
};
|
|
623
710
|
loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
|
|
624
711
|
for (const ref of collectWatchRefs(instance))
|
|
625
|
-
await loadInto(
|
|
712
|
+
await loadInto(
|
|
713
|
+
ref.id,
|
|
714
|
+
readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
|
|
715
|
+
);
|
|
626
716
|
return buildSnapshot({ docs: loaded });
|
|
627
717
|
}
|
|
628
718
|
async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
|
|
@@ -637,7 +727,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
|
|
|
637
727
|
return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
|
|
638
728
|
}
|
|
639
729
|
async function readDoc(client, id, perspective) {
|
|
640
|
-
return perspective ===
|
|
730
|
+
return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
|
|
641
731
|
}
|
|
642
732
|
function resourceFromParsed(parsed) {
|
|
643
733
|
return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
|
|
@@ -738,23 +828,7 @@ async function queryGuardsAcross(clients, query, params) {
|
|
|
738
828
|
function dedupById(guards) {
|
|
739
829
|
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
740
830
|
}
|
|
741
|
-
const
|
|
742
|
-
function validateTags(tags) {
|
|
743
|
-
if (tags.length === 0)
|
|
744
|
-
throw new Error("tags: must be a non-empty array");
|
|
745
|
-
for (const tag of tags)
|
|
746
|
-
if (!TAG_RE.test(tag))
|
|
747
|
-
throw new Error(
|
|
748
|
-
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
function canonicalTag(tags) {
|
|
752
|
-
return tags[0];
|
|
753
|
-
}
|
|
754
|
-
function tagScopeFilter(param = "engineTags") {
|
|
755
|
-
return `count(tags[@ in $${param}]) > 0`;
|
|
756
|
-
}
|
|
757
|
-
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
831
|
+
const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
758
832
|
function resolveGuard(guard, instance, stageName, now) {
|
|
759
833
|
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
760
834
|
if (targets === null) return null;
|
|
@@ -781,11 +855,11 @@ function resolveGuard(guard, instance, stageName, now) {
|
|
|
781
855
|
}
|
|
782
856
|
async function upsertGuard(client, doc) {
|
|
783
857
|
if (!await client.getDocument(doc._id)) {
|
|
784
|
-
await client.create(doc);
|
|
858
|
+
await client.create(doc, SYNC_COMMIT);
|
|
785
859
|
return;
|
|
786
860
|
}
|
|
787
861
|
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
788
|
-
await client.patch(doc._id).set(body).commit();
|
|
862
|
+
await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
|
|
789
863
|
}
|
|
790
864
|
function resolvedStageGuards(args) {
|
|
791
865
|
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
@@ -808,16 +882,16 @@ async function retractStageGuards(args) {
|
|
|
808
882
|
const live = await committedInstance(args);
|
|
809
883
|
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
810
884
|
for (const { client, doc } of resolvedStageGuards(args))
|
|
811
|
-
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
885
|
+
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
|
|
812
886
|
}
|
|
813
887
|
async function deleteOrphanedDefinitionGuards(args) {
|
|
814
|
-
const { client, clientForGdr, workflowResource, definition, definitions,
|
|
888
|
+
const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
|
|
815
889
|
client,
|
|
816
890
|
clientForGdr,
|
|
817
891
|
workflowResource,
|
|
818
892
|
definition,
|
|
819
893
|
definitions
|
|
820
|
-
}), ownPartitionPrefix = `${
|
|
894
|
+
}), ownPartitionPrefix = `${tag}.`;
|
|
821
895
|
let count = 0;
|
|
822
896
|
for (const { client: resourceClient, guards } of perClient) {
|
|
823
897
|
const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
|
|
@@ -832,16 +906,32 @@ function randomKey(length = 12) {
|
|
|
832
906
|
const bytes = new Uint8Array(length);
|
|
833
907
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
834
908
|
}
|
|
835
|
-
|
|
909
|
+
function isUnevaluable(result) {
|
|
910
|
+
return result == null;
|
|
911
|
+
}
|
|
912
|
+
async function evaluateConditionOutcome(args) {
|
|
836
913
|
const { condition, snapshot, params } = args;
|
|
837
|
-
|
|
914
|
+
if (condition === void 0) return "satisfied";
|
|
915
|
+
const result = await runGroq(condition, params, snapshot);
|
|
916
|
+
return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
|
|
917
|
+
}
|
|
918
|
+
async function evaluateCondition(args) {
|
|
919
|
+
return await evaluateConditionOutcome(args) === "satisfied";
|
|
838
920
|
}
|
|
839
921
|
async function evaluatePredicates(args) {
|
|
840
922
|
const out = {};
|
|
841
|
-
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
842
|
-
|
|
923
|
+
for (const [name, groq] of Object.entries(args.predicates ?? {})) {
|
|
924
|
+
const result = await runGroq(groq, args.params, args.snapshot);
|
|
925
|
+
out[name] = isUnevaluable(result) ? null : !!result;
|
|
926
|
+
}
|
|
843
927
|
return out;
|
|
844
928
|
}
|
|
929
|
+
async function evaluateRequirements(args) {
|
|
930
|
+
const unmet = [];
|
|
931
|
+
for (const [name, condition] of Object.entries(args.requirements ?? {}))
|
|
932
|
+
await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
|
|
933
|
+
return unmet;
|
|
934
|
+
}
|
|
845
935
|
async function runGroq(groq, params, snapshot) {
|
|
846
936
|
const tree = groqJs.parse(groq, { params });
|
|
847
937
|
return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
@@ -1006,10 +1096,10 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
|
|
|
1006
1096
|
stage: ctx.stageName ?? null,
|
|
1007
1097
|
task: ctx.taskName ?? null,
|
|
1008
1098
|
now: ctx.now,
|
|
1009
|
-
|
|
1099
|
+
tag: ctx.tag
|
|
1010
1100
|
});
|
|
1011
1101
|
try {
|
|
1012
|
-
const fetchOptions =
|
|
1102
|
+
const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
|
|
1013
1103
|
return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
|
|
1014
1104
|
} catch (err) {
|
|
1015
1105
|
throw new Error(
|
|
@@ -1139,13 +1229,16 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1139
1229
|
params
|
|
1140
1230
|
}), ...params };
|
|
1141
1231
|
}
|
|
1142
|
-
async function
|
|
1143
|
-
return condition === void 0 ?
|
|
1232
|
+
async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
|
|
1233
|
+
return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
|
|
1144
1234
|
condition,
|
|
1145
1235
|
snapshot: ctx.snapshot,
|
|
1146
1236
|
params: await ctxConditionParams(ctx, opts)
|
|
1147
1237
|
});
|
|
1148
1238
|
}
|
|
1239
|
+
async function ctxEvaluateCondition(ctx, condition, opts) {
|
|
1240
|
+
return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
|
|
1241
|
+
}
|
|
1149
1242
|
async function buildEngineContext(args) {
|
|
1150
1243
|
const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
|
|
1151
1244
|
return {
|
|
@@ -1173,7 +1266,7 @@ async function resolveStageStateEntries(args) {
|
|
|
1173
1266
|
client,
|
|
1174
1267
|
now,
|
|
1175
1268
|
selfId: instance._id,
|
|
1176
|
-
|
|
1269
|
+
tag: instance.tag,
|
|
1177
1270
|
stageName: stage.name,
|
|
1178
1271
|
workflowResource: instance.workflowResource,
|
|
1179
1272
|
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
@@ -1193,7 +1286,7 @@ async function resolveTaskStateEntries(args) {
|
|
|
1193
1286
|
client,
|
|
1194
1287
|
now,
|
|
1195
1288
|
selfId: instance._id,
|
|
1196
|
-
|
|
1289
|
+
tag: instance.tag,
|
|
1197
1290
|
stageName: stage.name,
|
|
1198
1291
|
workflowResource: instance.workflowResource,
|
|
1199
1292
|
taskName: task.name,
|
|
@@ -1265,7 +1358,7 @@ async function deployOrRollback(args) {
|
|
|
1265
1358
|
});
|
|
1266
1359
|
try {
|
|
1267
1360
|
let patch = args.client.patch(args.instanceId).set(args.restore);
|
|
1268
|
-
args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
|
|
1361
|
+
args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit(SYNC_COMMIT);
|
|
1269
1362
|
} catch (rollbackError) {
|
|
1270
1363
|
throw new WorkflowStateDivergedError({
|
|
1271
1364
|
instanceId: args.instanceId,
|
|
@@ -1372,7 +1465,7 @@ async function persist(ctx, mutation) {
|
|
|
1372
1465
|
lastChangedAt: ctx.now
|
|
1373
1466
|
}, pendingCreates = mutation.pendingCreates;
|
|
1374
1467
|
if (pendingCreates.length === 0)
|
|
1375
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1468
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
|
|
1376
1469
|
const tx = ctx.client.transaction();
|
|
1377
1470
|
for (const body of pendingCreates) tx.create(body);
|
|
1378
1471
|
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
@@ -1415,6 +1508,16 @@ function findCurrentStageEntry(instance) {
|
|
|
1415
1508
|
function findCurrentTasks(instance) {
|
|
1416
1509
|
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
1417
1510
|
}
|
|
1511
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1512
|
+
function validateTag(tag) {
|
|
1513
|
+
if (!TAG_RE.test(tag))
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`tag: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
function tagScopeFilter() {
|
|
1519
|
+
return "tag == $tag";
|
|
1520
|
+
}
|
|
1418
1521
|
function definitionLookupGroq(explicit) {
|
|
1419
1522
|
const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
|
|
1420
1523
|
return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
|
|
@@ -1431,6 +1534,14 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
1431
1534
|
return defaultClient;
|
|
1432
1535
|
}
|
|
1433
1536
|
}
|
|
1537
|
+
async function reload(client, instanceId, tag) {
|
|
1538
|
+
const doc = await client.getDocument(instanceId);
|
|
1539
|
+
if (!doc)
|
|
1540
|
+
throw new Error(`Workflow instance ${instanceId} not found`);
|
|
1541
|
+
if (doc.tag !== tag)
|
|
1542
|
+
throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
|
|
1543
|
+
return doc;
|
|
1544
|
+
}
|
|
1434
1545
|
function effectsContextEntry(name, value) {
|
|
1435
1546
|
const _key = randomKey();
|
|
1436
1547
|
return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
|
|
@@ -1446,7 +1557,7 @@ function buildInstanceBase(args) {
|
|
|
1446
1557
|
_rev: "",
|
|
1447
1558
|
_createdAt: now,
|
|
1448
1559
|
_updatedAt: now,
|
|
1449
|
-
|
|
1560
|
+
tag: args.tag,
|
|
1450
1561
|
workflowResource: args.workflowResource,
|
|
1451
1562
|
definition: args.definitionName,
|
|
1452
1563
|
pinnedVersion: args.pinnedVersion,
|
|
@@ -1497,7 +1608,7 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1497
1608
|
`Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
|
|
1498
1609
|
);
|
|
1499
1610
|
}
|
|
1500
|
-
const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.
|
|
1611
|
+
const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tag);
|
|
1501
1612
|
if (definition === null) {
|
|
1502
1613
|
const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
|
|
1503
1614
|
throw new Error(
|
|
@@ -1507,9 +1618,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1507
1618
|
const parentScope = await ctxConditionParams(ctx, {
|
|
1508
1619
|
taskName: task.name,
|
|
1509
1620
|
...actor ? { actor } : {}
|
|
1510
|
-
}), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
|
|
1621
|
+
}), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
|
|
1511
1622
|
for (const row of rows) {
|
|
1512
|
-
const initialState = await projectRowState(
|
|
1623
|
+
const initialState = await projectRowState(
|
|
1624
|
+
ctx,
|
|
1625
|
+
sub,
|
|
1626
|
+
definition,
|
|
1627
|
+
parentScope,
|
|
1628
|
+
row,
|
|
1629
|
+
discoveryResource
|
|
1630
|
+
), { ref, body } = await prepareChildInstance({
|
|
1513
1631
|
client: ctx.client,
|
|
1514
1632
|
parent: ctx.instance,
|
|
1515
1633
|
definition,
|
|
@@ -1524,28 +1642,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1524
1642
|
}
|
|
1525
1643
|
async function resolveSpawnRows(ctx, sub) {
|
|
1526
1644
|
const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
|
|
1527
|
-
let value;
|
|
1645
|
+
let value, discoveryResource;
|
|
1528
1646
|
if (groq.includes("*")) {
|
|
1529
|
-
const routingUri =
|
|
1530
|
-
value = await discoverItems({
|
|
1647
|
+
const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
|
|
1648
|
+
client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
|
|
1531
1649
|
client,
|
|
1532
1650
|
groq,
|
|
1533
1651
|
params,
|
|
1534
|
-
|
|
1652
|
+
// Draft-aware by default (drafts overlay published) when there's no
|
|
1653
|
+
// release, so a `forEach` discovers in-flight items the same way the
|
|
1654
|
+
// engine reads any other content.
|
|
1655
|
+
perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
|
|
1535
1656
|
});
|
|
1536
1657
|
} else {
|
|
1537
1658
|
const tree = groqJs.parse(groq, { params });
|
|
1538
1659
|
value = await (await groqJs.evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
|
|
1539
1660
|
}
|
|
1540
|
-
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
1661
|
+
return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
|
|
1541
1662
|
}
|
|
1542
|
-
function
|
|
1543
|
-
if (entries) {
|
|
1544
|
-
for (const s of entries)
|
|
1545
|
-
if (s._type === "doc.ref" && s.value !== null) return s.value.id;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
1663
|
+
async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
|
|
1549
1664
|
const out = [];
|
|
1550
1665
|
for (const [name, groq] of Object.entries(sub.with ?? {})) {
|
|
1551
1666
|
const declared = (childDefinition.state ?? []).find((e) => e.name === name);
|
|
@@ -1553,7 +1668,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
|
1553
1668
|
throw new Error(
|
|
1554
1669
|
`subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
|
|
1555
1670
|
);
|
|
1556
|
-
const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw,
|
|
1671
|
+
const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
|
|
1672
|
+
parentResource: ctx.instance.workflowResource,
|
|
1673
|
+
discoveryResource,
|
|
1674
|
+
entryName: name,
|
|
1675
|
+
childName: childDefinition.name
|
|
1676
|
+
});
|
|
1557
1677
|
value != null && out.push({
|
|
1558
1678
|
type: declared.type,
|
|
1559
1679
|
name,
|
|
@@ -1570,11 +1690,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
|
|
|
1570
1690
|
}
|
|
1571
1691
|
return out;
|
|
1572
1692
|
}
|
|
1573
|
-
function canonicaliseSpawnValue(kind, value,
|
|
1574
|
-
return kind === "doc.ref" ? coerceSpawnGdr(value,
|
|
1693
|
+
function canonicaliseSpawnValue(kind, value, cx) {
|
|
1694
|
+
return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
|
|
1695
|
+
}
|
|
1696
|
+
function assertBareIdRootsCorrectly(id, cx) {
|
|
1697
|
+
if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
`subworkflows.with["${cx.entryName}"]: subworkflow "${cx.childName}" projected the bare id "${id}", which roots at the parent resource "${cx.parentResource.id}", but spawn discovery ran against "${cx.discoveryResource.id}". The child would reference a document that doesn't exist in the parent resource. Project an explicit GDR URI in the with-GROQ (e.g. "dataset:<projectId>:<dataset>:" + _id) so the child's subject points at the resource the discovered rows came from.`
|
|
1700
|
+
);
|
|
1575
1701
|
}
|
|
1576
|
-
function coerceSpawnGdr(raw,
|
|
1577
|
-
const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(
|
|
1702
|
+
function coerceSpawnGdr(raw, cx) {
|
|
1703
|
+
const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
|
|
1578
1704
|
if (raw == null) return null;
|
|
1579
1705
|
if (typeof raw == "object" && "id" in raw && "type" in raw) {
|
|
1580
1706
|
const r = raw;
|
|
@@ -1587,15 +1713,15 @@ function coerceSpawnGdr(raw, workflowResource) {
|
|
|
1587
1713
|
}
|
|
1588
1714
|
return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
|
|
1589
1715
|
}
|
|
1590
|
-
async function resolveDefinitionRef(client, ref,
|
|
1591
|
-
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name,
|
|
1716
|
+
async function resolveDefinitionRef(client, ref, tag) {
|
|
1717
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
|
|
1592
1718
|
return wantsExplicit && (params.version = ref.version), await client.fetch(
|
|
1593
1719
|
definitionLookupGroq(wantsExplicit),
|
|
1594
1720
|
params
|
|
1595
1721
|
) ?? null;
|
|
1596
1722
|
}
|
|
1597
1723
|
async function prepareChildInstance(args) {
|
|
1598
|
-
const { client, parent, definition, initialState, effectsContext, actor, now } = args,
|
|
1724
|
+
const { client, parent, definition, initialState, effectsContext, actor, now } = args, childTag = parent.tag, workflowResource = parent.workflowResource, childDocId = `${childTag}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
|
|
1599
1725
|
...parent.ancestors,
|
|
1600
1726
|
{
|
|
1601
1727
|
id: selfGdr(parent),
|
|
@@ -1608,7 +1734,7 @@ async function prepareChildInstance(args) {
|
|
|
1608
1734
|
client,
|
|
1609
1735
|
now,
|
|
1610
1736
|
selfId: childDocId,
|
|
1611
|
-
|
|
1737
|
+
tag: childTag,
|
|
1612
1738
|
workflowResource,
|
|
1613
1739
|
...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
|
|
1614
1740
|
},
|
|
@@ -1627,7 +1753,7 @@ async function prepareChildInstance(args) {
|
|
|
1627
1753
|
), base = buildInstanceBase({
|
|
1628
1754
|
id: childDocId,
|
|
1629
1755
|
now,
|
|
1630
|
-
|
|
1756
|
+
tag: childTag,
|
|
1631
1757
|
workflowResource,
|
|
1632
1758
|
definitionName: definition.name,
|
|
1633
1759
|
pinnedVersion: definition.version,
|
|
@@ -2073,21 +2199,24 @@ function resolveMachineStep(mutation, task, entry, now) {
|
|
|
2073
2199
|
async function buildStageTasks(ctx, stage) {
|
|
2074
2200
|
const entries = [];
|
|
2075
2201
|
for (const task of stage.tasks ?? []) {
|
|
2076
|
-
const
|
|
2202
|
+
const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
|
|
2077
2203
|
entries.push({
|
|
2078
2204
|
_key: randomKey(),
|
|
2079
2205
|
name: task.name,
|
|
2080
2206
|
status: inScope ? "pending" : "skipped",
|
|
2081
|
-
...task.filter !== void 0 ? {
|
|
2082
|
-
filterEvaluation: {
|
|
2083
|
-
at: ctx.now,
|
|
2084
|
-
truthy: inScope
|
|
2085
|
-
}
|
|
2086
|
-
} : {}
|
|
2207
|
+
...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
|
|
2087
2208
|
});
|
|
2088
2209
|
}
|
|
2089
2210
|
return entries;
|
|
2090
2211
|
}
|
|
2212
|
+
function buildFilterEvaluation(at, filter, outcome) {
|
|
2213
|
+
return outcome === "unevaluable" ? {
|
|
2214
|
+
at,
|
|
2215
|
+
truthy: !1,
|
|
2216
|
+
unevaluable: !0,
|
|
2217
|
+
detail: `Filter "${filter}" could not be evaluated (GROQ null \u2014 a referenced operand was missing or incomparable). Task kept in scope so the gate is not silently skipped.`
|
|
2218
|
+
} : { at, truthy: outcome === "satisfied" };
|
|
2219
|
+
}
|
|
2091
2220
|
function isTerminalStage(stage) {
|
|
2092
2221
|
return (stage.transitions ?? []).length === 0;
|
|
2093
2222
|
}
|
|
@@ -2237,8 +2366,11 @@ async function commitTransition(ctx, actor) {
|
|
|
2237
2366
|
};
|
|
2238
2367
|
}
|
|
2239
2368
|
async function pickTransition(ctx, stage) {
|
|
2240
|
-
for (const candidate of stage.transitions ?? [])
|
|
2241
|
-
|
|
2369
|
+
for (const candidate of stage.transitions ?? []) {
|
|
2370
|
+
const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
|
|
2371
|
+
if (outcome === "satisfied") return candidate;
|
|
2372
|
+
if (outcome === "unevaluable") return;
|
|
2373
|
+
}
|
|
2242
2374
|
}
|
|
2243
2375
|
async function evaluateAutoTransitions(args) {
|
|
2244
2376
|
const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
|
|
@@ -2268,7 +2400,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
2268
2400
|
}, committed = await client.patch(instance._id).set({
|
|
2269
2401
|
stages: [initialStageEntry],
|
|
2270
2402
|
lastChangedAt: now
|
|
2271
|
-
}).ifRevisionId(instance._rev).commit();
|
|
2403
|
+
}).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
|
|
2272
2404
|
await deployOrRollback({
|
|
2273
2405
|
client,
|
|
2274
2406
|
instanceId: instance._id,
|
|
@@ -2493,17 +2625,12 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2493
2625
|
}
|
|
2494
2626
|
}
|
|
2495
2627
|
async function evaluateInstance(args) {
|
|
2496
|
-
const { client,
|
|
2497
|
-
|
|
2628
|
+
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2629
|
+
validateTag(tag);
|
|
2498
2630
|
const { actor, grants } = await resolveAccess(client, {
|
|
2499
2631
|
...args.access !== void 0 ? { override: args.access } : {},
|
|
2500
2632
|
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
2501
|
-
}), instance = await client
|
|
2502
|
-
if (!instance)
|
|
2503
|
-
throw new Error(`Workflow instance "${instanceId}" not found`);
|
|
2504
|
-
if (!tags.some((t) => instance.tags?.includes(t)))
|
|
2505
|
-
throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
|
|
2506
|
-
const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
|
|
2633
|
+
}), instance = await reload(client, instanceId, tag), definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
|
|
2507
2634
|
return evaluateFromSnapshot({
|
|
2508
2635
|
instance,
|
|
2509
2636
|
definition,
|
|
@@ -2535,15 +2662,18 @@ async function evaluateFromSnapshot(args) {
|
|
|
2535
2662
|
})
|
|
2536
2663
|
);
|
|
2537
2664
|
const transitionEvaluations = [];
|
|
2538
|
-
for (const transition of stage.transitions ?? [])
|
|
2665
|
+
for (const transition of stage.transitions ?? []) {
|
|
2666
|
+
const outcome = await evaluateConditionOutcome({
|
|
2667
|
+
condition: transition.filter,
|
|
2668
|
+
snapshot,
|
|
2669
|
+
params: scope
|
|
2670
|
+
});
|
|
2539
2671
|
transitionEvaluations.push({
|
|
2540
2672
|
transition,
|
|
2541
|
-
filterSatisfied:
|
|
2542
|
-
|
|
2543
|
-
snapshot,
|
|
2544
|
-
params: scope
|
|
2545
|
-
})
|
|
2673
|
+
filterSatisfied: outcome === "satisfied",
|
|
2674
|
+
unevaluable: outcome === "unevaluable"
|
|
2546
2675
|
});
|
|
2676
|
+
}
|
|
2547
2677
|
const currentStage = {
|
|
2548
2678
|
stage,
|
|
2549
2679
|
tasks: taskEvaluations,
|
|
@@ -2591,7 +2721,11 @@ async function evaluateTask(args) {
|
|
|
2591
2721
|
...scopedStateOverlay(instance, snapshot, task.name)
|
|
2592
2722
|
},
|
|
2593
2723
|
assigned
|
|
2594
|
-
},
|
|
2724
|
+
}, unmetRequirements = await evaluateRequirements({
|
|
2725
|
+
requirements: task.requirements,
|
|
2726
|
+
snapshot,
|
|
2727
|
+
params: taskScope
|
|
2728
|
+
}), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
|
|
2595
2729
|
for (const action of task.actions ?? [])
|
|
2596
2730
|
actions.push(
|
|
2597
2731
|
await evaluateAction({
|
|
@@ -2601,7 +2735,8 @@ async function evaluateTask(args) {
|
|
|
2601
2735
|
snapshot,
|
|
2602
2736
|
taskScope,
|
|
2603
2737
|
stageHasExits,
|
|
2604
|
-
guardDenial
|
|
2738
|
+
guardDenial,
|
|
2739
|
+
requirementsReason
|
|
2605
2740
|
})
|
|
2606
2741
|
);
|
|
2607
2742
|
return {
|
|
@@ -2612,12 +2747,22 @@ async function evaluateTask(args) {
|
|
|
2612
2747
|
// they're still inbox items. The inbox reads the assignees-kind state
|
|
2613
2748
|
// entry BY KIND (who-data is state).
|
|
2614
2749
|
pendingOnActor: (status === "active" || status === "pending") && assigned,
|
|
2750
|
+
...unmetRequirements.length > 0 ? { unmetRequirements } : {},
|
|
2615
2751
|
actions
|
|
2616
2752
|
};
|
|
2617
2753
|
}
|
|
2618
2754
|
async function evaluateAction(args) {
|
|
2619
|
-
const {
|
|
2620
|
-
|
|
2755
|
+
const {
|
|
2756
|
+
action,
|
|
2757
|
+
status,
|
|
2758
|
+
instance,
|
|
2759
|
+
snapshot,
|
|
2760
|
+
taskScope,
|
|
2761
|
+
stageHasExits,
|
|
2762
|
+
guardDenial,
|
|
2763
|
+
requirementsReason
|
|
2764
|
+
} = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
|
|
2765
|
+
return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : requirementsReason !== void 0 ? disabled(action, requirementsReason) : action.filter !== void 0 && !await evaluateCondition({
|
|
2621
2766
|
condition: action.filter,
|
|
2622
2767
|
snapshot,
|
|
2623
2768
|
params: taskScope
|
|
@@ -2644,22 +2789,21 @@ function lifecycleReason(instance, status, stageHasExits) {
|
|
|
2644
2789
|
function disabled(action, reason) {
|
|
2645
2790
|
return { action, allowed: !1, disabledReason: reason };
|
|
2646
2791
|
}
|
|
2647
|
-
function definitionDocId(_workflowResource,
|
|
2648
|
-
return `${
|
|
2792
|
+
function definitionDocId(_workflowResource, tag, definition, version) {
|
|
2793
|
+
return `${tag}.${definition}.v${version}`;
|
|
2649
2794
|
}
|
|
2650
|
-
async function sortByDependencies(client, definitions,
|
|
2651
|
-
const
|
|
2795
|
+
async function sortByDependencies(client, definitions, tag, workflowResource) {
|
|
2796
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2652
2797
|
for (const def of definitions) {
|
|
2653
2798
|
const list = byName.get(def.name) ?? [];
|
|
2654
2799
|
list.push(def), byName.set(def.name, list);
|
|
2655
2800
|
}
|
|
2656
2801
|
const findInBatch = (ref) => findRefInBatch(byName, ref);
|
|
2657
2802
|
return await assertCrossBatchRefsDeployed(client, definitions, {
|
|
2658
|
-
|
|
2803
|
+
tag,
|
|
2659
2804
|
workflowResource,
|
|
2660
|
-
prefix,
|
|
2661
2805
|
findInBatch
|
|
2662
|
-
}), topoSortDefinitions(definitions, { workflowResource,
|
|
2806
|
+
}), topoSortDefinitions(definitions, { workflowResource, tag, findInBatch });
|
|
2663
2807
|
}
|
|
2664
2808
|
function findRefInBatch(byName, ref) {
|
|
2665
2809
|
const candidates = byName.get(ref.name);
|
|
@@ -2672,10 +2816,10 @@ function findRefInBatch(byName, ref) {
|
|
|
2672
2816
|
async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
2673
2817
|
const missing = [];
|
|
2674
2818
|
for (const def of definitions) {
|
|
2675
|
-
const fromId = definitionDocId(ctx.workflowResource, ctx.
|
|
2819
|
+
const fromId = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
|
|
2676
2820
|
for (const ref of refsOf(def)) {
|
|
2677
2821
|
if (ctx.findInBatch(ref) !== void 0) continue;
|
|
2678
|
-
const label = await resolveDeployedRefLabel(client, ref, ctx.
|
|
2822
|
+
const label = await resolveDeployedRefLabel(client, ref, ctx.tag);
|
|
2679
2823
|
label !== void 0 && missing.push({ from: fromId, ref: label });
|
|
2680
2824
|
}
|
|
2681
2825
|
}
|
|
@@ -2687,8 +2831,8 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
|
2687
2831
|
`)
|
|
2688
2832
|
);
|
|
2689
2833
|
}
|
|
2690
|
-
async function resolveDeployedRefLabel(client, ref,
|
|
2691
|
-
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name,
|
|
2834
|
+
async function resolveDeployedRefLabel(client, ref, tag) {
|
|
2835
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
|
|
2692
2836
|
if (wantsExplicit && (params.version = ref.version), !await client.fetch(
|
|
2693
2837
|
definitionLookupGroq(wantsExplicit),
|
|
2694
2838
|
params
|
|
@@ -2697,7 +2841,7 @@ async function resolveDeployedRefLabel(client, ref, tags) {
|
|
|
2697
2841
|
}
|
|
2698
2842
|
function topoSortDefinitions(definitions, ctx) {
|
|
2699
2843
|
const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
|
|
2700
|
-
const id = definitionDocId(ctx.workflowResource, ctx.
|
|
2844
|
+
const id = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
|
|
2701
2845
|
if (!visited.has(id)) {
|
|
2702
2846
|
if (visiting.has(id))
|
|
2703
2847
|
throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
|
|
@@ -2738,11 +2882,11 @@ function stableStringify(value) {
|
|
|
2738
2882
|
([a], [b]) => a.localeCompare(b)
|
|
2739
2883
|
).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
|
|
2740
2884
|
}
|
|
2741
|
-
async function loadDefinition(client, definition, version,
|
|
2885
|
+
async function loadDefinition(client, definition, version, tag) {
|
|
2742
2886
|
if (version !== void 0) {
|
|
2743
2887
|
const doc = await client.fetch(
|
|
2744
2888
|
definitionLookupGroq(!0),
|
|
2745
|
-
{ definition, version,
|
|
2889
|
+
{ definition, version, tag }
|
|
2746
2890
|
);
|
|
2747
2891
|
if (!doc)
|
|
2748
2892
|
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
@@ -2750,16 +2894,16 @@ async function loadDefinition(client, definition, version, tags) {
|
|
|
2750
2894
|
}
|
|
2751
2895
|
const latest = await client.fetch(definitionLookupGroq(!1), {
|
|
2752
2896
|
definition,
|
|
2753
|
-
|
|
2897
|
+
tag
|
|
2754
2898
|
});
|
|
2755
2899
|
if (!latest)
|
|
2756
2900
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2757
2901
|
return latest;
|
|
2758
2902
|
}
|
|
2759
|
-
async function loadDefinitionVersions(client, definition,
|
|
2903
|
+
async function loadDefinitionVersions(client, definition, tag) {
|
|
2760
2904
|
return client.fetch(
|
|
2761
|
-
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition &&
|
|
2762
|
-
{ definition,
|
|
2905
|
+
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}] | order(version desc)`,
|
|
2906
|
+
{ definition, tag }
|
|
2763
2907
|
);
|
|
2764
2908
|
}
|
|
2765
2909
|
async function abortInstance(args) {
|
|
@@ -2895,7 +3039,8 @@ const disabledReasonDetail = {
|
|
|
2895
3039
|
"task-not-active": (r) => `task status is "${r.status}"`,
|
|
2896
3040
|
"stage-terminal": (r) => `stage "${r.stage}" is terminal`,
|
|
2897
3041
|
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
2898
|
-
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}
|
|
3042
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
|
|
3043
|
+
"requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
|
|
2899
3044
|
};
|
|
2900
3045
|
function formatDisabledReason(task, action, reason) {
|
|
2901
3046
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
@@ -2910,7 +3055,7 @@ function bareIdFromSpawnRef(uri) {
|
|
|
2910
3055
|
}
|
|
2911
3056
|
}
|
|
2912
3057
|
async function resolveOperationContext(args) {
|
|
2913
|
-
|
|
3058
|
+
validateTag(args.tag);
|
|
2914
3059
|
const access = await resolveAccess(args.client, {
|
|
2915
3060
|
...args.access !== void 0 ? { override: args.access } : {},
|
|
2916
3061
|
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
@@ -2944,16 +3089,6 @@ async function abortAndPropagate(args) {
|
|
|
2944
3089
|
function buildClientForGdr(defaultClient, resolver) {
|
|
2945
3090
|
return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
|
|
2946
3091
|
}
|
|
2947
|
-
async function reload(client, instanceId, tags) {
|
|
2948
|
-
const doc = await client.getDocument(instanceId);
|
|
2949
|
-
if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
|
|
2950
|
-
if (!intersectsTags(doc.tags, tags))
|
|
2951
|
-
throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
|
|
2952
|
-
return doc;
|
|
2953
|
-
}
|
|
2954
|
-
function intersectsTags(docTags, engineTags) {
|
|
2955
|
-
return !docTags || docTags.length === 0 ? !1 : engineTags.some((t) => docTags.includes(t));
|
|
2956
|
-
}
|
|
2957
3092
|
function toEffectsContextEntries(ctx) {
|
|
2958
3093
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
2959
3094
|
}
|
|
@@ -2979,15 +3114,15 @@ async function applyAction(args) {
|
|
|
2979
3114
|
})).ranOps;
|
|
2980
3115
|
}
|
|
2981
3116
|
async function deleteDefinition(args) {
|
|
2982
|
-
const { client,
|
|
3117
|
+
const { client, tag, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tag);
|
|
2983
3118
|
if (versions.length === 0)
|
|
2984
3119
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2985
3120
|
const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
|
|
2986
3121
|
if (targets.length === 0)
|
|
2987
3122
|
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
2988
3123
|
const lastVersionGoes = targets.length === versions.length;
|
|
2989
|
-
await assertNoSpawnReferrers(client,
|
|
2990
|
-
const instanceIds = await nonTerminalInstanceIds(client,
|
|
3124
|
+
await assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes);
|
|
3125
|
+
const instanceIds = await nonTerminalInstanceIds(client, tag, definition, version);
|
|
2991
3126
|
if (instanceIds.length > 0 && cascade2 !== !0)
|
|
2992
3127
|
throw new Error(
|
|
2993
3128
|
`Cannot delete ${definition}: ${instanceIds.length} non-terminal instance(s) exist (${previewIds(instanceIds)}). Pass cascade to abort them first \u2014 instances are aborted in place, never deleted.`
|
|
@@ -3008,7 +3143,7 @@ async function deleteDefinition(args) {
|
|
|
3008
3143
|
workflowResource: args.workflowResource,
|
|
3009
3144
|
definition,
|
|
3010
3145
|
definitions: versions,
|
|
3011
|
-
|
|
3146
|
+
tag
|
|
3012
3147
|
}) : 0;
|
|
3013
3148
|
return {
|
|
3014
3149
|
name: definition,
|
|
@@ -3017,10 +3152,10 @@ async function deleteDefinition(args) {
|
|
|
3017
3152
|
deletedGuardCount
|
|
3018
3153
|
};
|
|
3019
3154
|
}
|
|
3020
|
-
async function assertNoSpawnReferrers(client,
|
|
3155
|
+
async function assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes) {
|
|
3021
3156
|
const deployed = await client.fetch(
|
|
3022
3157
|
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
|
|
3023
|
-
{
|
|
3158
|
+
{ tag }
|
|
3024
3159
|
), targetIds = new Set(targets.map((d) => d._id)), targetVersions = new Set(targets.map((d) => d.version)), referrers = deployed.filter((d) => !targetIds.has(d._id)).filter(
|
|
3025
3160
|
(d) => refsOf(d).some(
|
|
3026
3161
|
(ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
|
|
@@ -3033,11 +3168,11 @@ async function assertNoSpawnReferrers(client, tags, definition, targets, lastVer
|
|
|
3033
3168
|
);
|
|
3034
3169
|
}
|
|
3035
3170
|
}
|
|
3036
|
-
async function nonTerminalInstanceIds(client,
|
|
3171
|
+
async function nonTerminalInstanceIds(client, tag, definition, version) {
|
|
3037
3172
|
const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
|
|
3038
3173
|
return (await client.fetch(
|
|
3039
3174
|
`*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
|
|
3040
|
-
{ definition,
|
|
3175
|
+
{ definition, tag, ...version !== void 0 ? { version } : {} }
|
|
3041
3176
|
)).map((doc) => doc._id);
|
|
3042
3177
|
}
|
|
3043
3178
|
async function abortInstances(args) {
|
|
@@ -3068,20 +3203,20 @@ const workflow = {
|
|
|
3068
3203
|
* Cycles in the dependency graph error before any write happens.
|
|
3069
3204
|
*/
|
|
3070
3205
|
deployDefinitions: async (args) => {
|
|
3071
|
-
const { client,
|
|
3072
|
-
|
|
3206
|
+
const { client, tag, workflowResource, definitions } = args;
|
|
3207
|
+
validateTag(tag);
|
|
3073
3208
|
for (const def of definitions)
|
|
3074
3209
|
validateDefinition(def);
|
|
3075
|
-
const ordered = await sortByDependencies(client, definitions,
|
|
3210
|
+
const ordered = await sortByDependencies(client, definitions, tag, workflowResource), results = [], tx = client.transaction();
|
|
3076
3211
|
let hasWrites = !1;
|
|
3077
3212
|
for (const def of ordered) {
|
|
3078
|
-
const id = definitionDocId(workflowResource,
|
|
3213
|
+
const id = definitionDocId(workflowResource, tag, def.name, def.version), expected = {
|
|
3079
3214
|
...def,
|
|
3080
3215
|
_id: id,
|
|
3081
3216
|
_type: schema.WORKFLOW_DEFINITION_TYPE,
|
|
3082
|
-
|
|
3217
|
+
tag
|
|
3083
3218
|
}, existing = await client.getDocument(id);
|
|
3084
|
-
if (existing &&
|
|
3219
|
+
if (existing && existing.tag === tag && isDefinitionUnchanged(existing, expected)) {
|
|
3085
3220
|
results.push({ definition: def.name, version: def.version, status: "unchanged" });
|
|
3086
3221
|
continue;
|
|
3087
3222
|
}
|
|
@@ -3119,7 +3254,7 @@ const workflow = {
|
|
|
3119
3254
|
startInstance: async (args) => {
|
|
3120
3255
|
const {
|
|
3121
3256
|
client,
|
|
3122
|
-
|
|
3257
|
+
tag,
|
|
3123
3258
|
workflowResource,
|
|
3124
3259
|
definition: definitionName,
|
|
3125
3260
|
version,
|
|
@@ -3128,7 +3263,7 @@ const workflow = {
|
|
|
3128
3263
|
effectsContext,
|
|
3129
3264
|
instanceId,
|
|
3130
3265
|
perspective
|
|
3131
|
-
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version,
|
|
3266
|
+
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tag), id = instanceId ?? `${tag}.wf-instance.${randomKey()}`;
|
|
3132
3267
|
if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
|
|
3133
3268
|
throw new Error(
|
|
3134
3269
|
`Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
|
|
@@ -3140,7 +3275,7 @@ const workflow = {
|
|
|
3140
3275
|
client,
|
|
3141
3276
|
now,
|
|
3142
3277
|
selfId: id,
|
|
3143
|
-
|
|
3278
|
+
tag,
|
|
3144
3279
|
workflowResource,
|
|
3145
3280
|
...perspective !== void 0 ? { perspective } : {}
|
|
3146
3281
|
},
|
|
@@ -3148,7 +3283,7 @@ const workflow = {
|
|
|
3148
3283
|
}), effectivePerspective = perspective ?? derivePerspectiveFromState(resolvedState), base = buildInstanceBase({
|
|
3149
3284
|
id,
|
|
3150
3285
|
now,
|
|
3151
|
-
|
|
3286
|
+
tag,
|
|
3152
3287
|
workflowResource,
|
|
3153
3288
|
definitionName: definition.name,
|
|
3154
3289
|
pinnedVersion: definition.version,
|
|
@@ -3160,7 +3295,7 @@ const workflow = {
|
|
|
3160
3295
|
initialStage: definition.initialStage,
|
|
3161
3296
|
actor
|
|
3162
3297
|
});
|
|
3163
|
-
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id,
|
|
3298
|
+
return await client.create(base, SYNC_COMMIT), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
|
|
3164
3299
|
},
|
|
3165
3300
|
/**
|
|
3166
3301
|
* Fire an action against a task. If the task is pending, it is
|
|
@@ -3175,19 +3310,19 @@ const workflow = {
|
|
|
3175
3310
|
fireAction: async (args) => {
|
|
3176
3311
|
const {
|
|
3177
3312
|
client,
|
|
3178
|
-
|
|
3313
|
+
tag,
|
|
3179
3314
|
instanceId,
|
|
3180
3315
|
task,
|
|
3181
3316
|
action,
|
|
3182
3317
|
params,
|
|
3183
3318
|
idempotent,
|
|
3184
3319
|
resourceClients
|
|
3185
|
-
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId,
|
|
3320
|
+
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3186
3321
|
if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
|
|
3187
3322
|
return { instance: before, cascaded: 0, fired: !1 };
|
|
3188
3323
|
const evaluation = await evaluateInstance({
|
|
3189
3324
|
client,
|
|
3190
|
-
|
|
3325
|
+
tag,
|
|
3191
3326
|
instanceId,
|
|
3192
3327
|
access,
|
|
3193
3328
|
clock,
|
|
@@ -3206,7 +3341,7 @@ const workflow = {
|
|
|
3206
3341
|
...params !== void 0 ? { params } : {}
|
|
3207
3342
|
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3208
3343
|
return {
|
|
3209
|
-
instance: await reload(client, instanceId,
|
|
3344
|
+
instance: await reload(client, instanceId, tag),
|
|
3210
3345
|
cascaded,
|
|
3211
3346
|
fired: !0,
|
|
3212
3347
|
...ranOps !== void 0 ? { ranOps } : {}
|
|
@@ -3220,7 +3355,7 @@ const workflow = {
|
|
|
3220
3355
|
* reference them. Cascades after.
|
|
3221
3356
|
*/
|
|
3222
3357
|
completeEffect: async (args) => {
|
|
3223
|
-
const { client,
|
|
3358
|
+
const { client, tag, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3224
3359
|
await completeEffect({
|
|
3225
3360
|
client,
|
|
3226
3361
|
instanceId,
|
|
@@ -3234,7 +3369,7 @@ const workflow = {
|
|
|
3234
3369
|
});
|
|
3235
3370
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3236
3371
|
return {
|
|
3237
|
-
instance: await reload(client, instanceId,
|
|
3372
|
+
instance: await reload(client, instanceId, tag),
|
|
3238
3373
|
cascaded,
|
|
3239
3374
|
fired: !0
|
|
3240
3375
|
};
|
|
@@ -3249,11 +3384,11 @@ const workflow = {
|
|
|
3249
3384
|
* affected instances and the engine re-evaluates.
|
|
3250
3385
|
*/
|
|
3251
3386
|
tick: async (args) => {
|
|
3252
|
-
const { client,
|
|
3387
|
+
const { client, tag, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload(client, instanceId, tag), guards = await verdictGuardsForInstance(client, current._id);
|
|
3253
3388
|
await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
|
|
3254
3389
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3255
3390
|
return {
|
|
3256
|
-
instance: await reload(client, instanceId,
|
|
3391
|
+
instance: await reload(client, instanceId, tag),
|
|
3257
3392
|
cascaded,
|
|
3258
3393
|
fired: cascaded > 0
|
|
3259
3394
|
};
|
|
@@ -3264,8 +3399,8 @@ const workflow = {
|
|
|
3264
3399
|
* upstream; this verb performs the mechanical move.
|
|
3265
3400
|
*/
|
|
3266
3401
|
setStage: async (args) => {
|
|
3267
|
-
const { client,
|
|
3268
|
-
await reload(client, instanceId,
|
|
3402
|
+
const { client, tag, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3403
|
+
await reload(client, instanceId, tag);
|
|
3269
3404
|
const result = await setStage({
|
|
3270
3405
|
client,
|
|
3271
3406
|
instanceId,
|
|
@@ -3274,7 +3409,7 @@ const workflow = {
|
|
|
3274
3409
|
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3275
3410
|
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
3276
3411
|
return {
|
|
3277
|
-
instance: await reload(client, instanceId,
|
|
3412
|
+
instance: await reload(client, instanceId, tag),
|
|
3278
3413
|
cascaded,
|
|
3279
3414
|
fired: result.fired
|
|
3280
3415
|
};
|
|
@@ -3286,28 +3421,28 @@ const workflow = {
|
|
|
3286
3421
|
* contract (propagated, not cascaded — the instance is terminal).
|
|
3287
3422
|
*/
|
|
3288
3423
|
abortInstance: async (args) => {
|
|
3289
|
-
const { client,
|
|
3290
|
-
await reload(client, instanceId,
|
|
3424
|
+
const { client, tag, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3425
|
+
await reload(client, instanceId, tag);
|
|
3291
3426
|
const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
|
|
3292
3427
|
return {
|
|
3293
|
-
instance: await reload(client, instanceId,
|
|
3428
|
+
instance: await reload(client, instanceId, tag),
|
|
3294
3429
|
cascaded: 0,
|
|
3295
3430
|
fired
|
|
3296
3431
|
};
|
|
3297
3432
|
},
|
|
3298
3433
|
/**
|
|
3299
|
-
* Fetch a workflow instance by id, scoped to the engine's
|
|
3434
|
+
* Fetch a workflow instance by id, scoped to the engine's tag.
|
|
3300
3435
|
* Throws when the instance doesn't exist or isn't visible to this
|
|
3301
3436
|
* engine.
|
|
3302
3437
|
*/
|
|
3303
3438
|
getInstance: async (args) => {
|
|
3304
|
-
const { client,
|
|
3305
|
-
return
|
|
3439
|
+
const { client, tag, instanceId } = args;
|
|
3440
|
+
return validateTag(tag), reload(client, instanceId, tag);
|
|
3306
3441
|
},
|
|
3307
3442
|
guardsForInstance: async (args) => {
|
|
3308
|
-
const { client,
|
|
3309
|
-
|
|
3310
|
-
const instance = await reload(client, instanceId,
|
|
3443
|
+
const { client, tag, instanceId, resourceClients } = args;
|
|
3444
|
+
validateTag(tag);
|
|
3445
|
+
const instance = await reload(client, instanceId, tag);
|
|
3311
3446
|
return guardsForInstance({
|
|
3312
3447
|
client,
|
|
3313
3448
|
clientForGdr: buildClientForGdr(client, resourceClients),
|
|
@@ -3315,9 +3450,9 @@ const workflow = {
|
|
|
3315
3450
|
});
|
|
3316
3451
|
},
|
|
3317
3452
|
guardsForDefinition: async (args) => {
|
|
3318
|
-
const { client,
|
|
3319
|
-
|
|
3320
|
-
const definitions = await loadDefinitionVersions(client, definition,
|
|
3453
|
+
const { client, tag, workflowResource, definition, resourceClients } = args;
|
|
3454
|
+
validateTag(tag);
|
|
3455
|
+
const definitions = await loadDefinitionVersions(client, definition, tag);
|
|
3321
3456
|
if (definitions.length === 0)
|
|
3322
3457
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
3323
3458
|
return guardsForDefinition({
|
|
@@ -3329,22 +3464,21 @@ const workflow = {
|
|
|
3329
3464
|
});
|
|
3330
3465
|
},
|
|
3331
3466
|
/**
|
|
3332
|
-
* Run a caller-supplied GROQ query with the engine's
|
|
3333
|
-
* `$
|
|
3467
|
+
* Run a caller-supplied GROQ query with the engine's tag bound as
|
|
3468
|
+
* `$tag`. This does NOT rewrite the query — arbitrary GROQ
|
|
3334
3469
|
* can't be safely tag-scoped after the fact — so the CALLER MUST
|
|
3335
|
-
* filter on `$
|
|
3336
|
-
*
|
|
3337
|
-
* cross-tenant reads, a query that never references `$engineTags` is
|
|
3470
|
+
* filter on `$tag` (e.g. `tag == $tag`). To guard against accidental
|
|
3471
|
+
* cross-partition reads, a query that never references `$tag` is
|
|
3338
3472
|
* rejected before it reaches the lake. Caller is responsible for type
|
|
3339
3473
|
* narrowing the result.
|
|
3340
3474
|
*/
|
|
3341
3475
|
query: async (args) => {
|
|
3342
|
-
const { client,
|
|
3343
|
-
if (
|
|
3476
|
+
const { client, tag, groq, params } = args;
|
|
3477
|
+
if (validateTag(tag), !/\$tag\b/.test(groq))
|
|
3344
3478
|
throw new Error(
|
|
3345
|
-
`workflow.query: query must filter on $
|
|
3479
|
+
`workflow.query: query must filter on $tag to stay tag-scoped (e.g. "tag == $tag"); got: ${groq}`
|
|
3346
3480
|
);
|
|
3347
|
-
return client.fetch(groq, { ...params,
|
|
3481
|
+
return client.fetch(groq, { ...params, tag });
|
|
3348
3482
|
},
|
|
3349
3483
|
/**
|
|
3350
3484
|
* Snapshot-aware GROQ — runs against the same in-memory view that
|
|
@@ -3363,9 +3497,9 @@ const workflow = {
|
|
|
3363
3497
|
* without re-implementing hydration. Pure read — never writes.
|
|
3364
3498
|
*/
|
|
3365
3499
|
queryInScope: async (args) => {
|
|
3366
|
-
const { client,
|
|
3367
|
-
|
|
3368
|
-
const instance = await reload(client, instanceId,
|
|
3500
|
+
const { client, tag, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
|
|
3501
|
+
validateTag(tag);
|
|
3502
|
+
const instance = await reload(client, instanceId, tag), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
|
|
3369
3503
|
return await (await groqJs.evaluate(tree, {
|
|
3370
3504
|
dataset: snapshot.docs,
|
|
3371
3505
|
params: { ...reserved, ...params }
|
|
@@ -3375,13 +3509,13 @@ const workflow = {
|
|
|
3375
3509
|
* List every pending effect on the instance. Returns the same entries
|
|
3376
3510
|
* the runtime would see — claimed and unclaimed alike.
|
|
3377
3511
|
*/
|
|
3378
|
-
listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.
|
|
3512
|
+
listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tag)).pendingEffects,
|
|
3379
3513
|
/**
|
|
3380
3514
|
* Filter pending effects on the instance by criteria. `claimed`
|
|
3381
3515
|
* filters on claim presence; `names` restricts to specific effect
|
|
3382
3516
|
* names. Both filters compose (AND).
|
|
3383
3517
|
*/
|
|
3384
|
-
findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.
|
|
3518
|
+
findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tag)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.name))),
|
|
3385
3519
|
/**
|
|
3386
3520
|
* Project the instance from a given actor's perspective. Returns a
|
|
3387
3521
|
* `WorkflowEvaluation` with per-action verdicts (`allowed` + a
|
|
@@ -3401,8 +3535,8 @@ const workflow = {
|
|
|
3401
3535
|
* Pure read.
|
|
3402
3536
|
*/
|
|
3403
3537
|
diagnose: async (args) => {
|
|
3404
|
-
const evaluation = await evaluateInstance(args);
|
|
3405
|
-
return { evaluation, diagnosis:
|
|
3538
|
+
const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
|
|
3539
|
+
return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
|
|
3406
3540
|
},
|
|
3407
3541
|
/**
|
|
3408
3542
|
* List the actions an actor could fire on an instance's current stage,
|
|
@@ -3422,18 +3556,18 @@ const workflow = {
|
|
|
3422
3556
|
* record. (The per-task `spawnedInstances` field on the active stage
|
|
3423
3557
|
* only exists while that stage is current; history survives.) Strips
|
|
3424
3558
|
* the GDR URI on each `instanceRef` to a bare `_id`, fetches the
|
|
3425
|
-
* instances, drops any that aren't visible to this engine's
|
|
3559
|
+
* instances, drops any that aren't visible to this engine's tag, and
|
|
3426
3560
|
* returns them sorted by `startedAt` ascending.
|
|
3427
3561
|
*
|
|
3428
3562
|
* Pass `task` to restrict to a single spawning task on the parent.
|
|
3429
3563
|
*/
|
|
3430
3564
|
children: async (args) => {
|
|
3431
|
-
const { client,
|
|
3432
|
-
|
|
3433
|
-
const parent = await reload(client, instanceId,
|
|
3565
|
+
const { client, tag, instanceId, task } = args;
|
|
3566
|
+
validateTag(tag);
|
|
3567
|
+
const parent = await reload(client, instanceId, tag), isSpawned = (h) => h._type === "spawned", ids = parent.history.filter(isSpawned).filter((h) => task === void 0 || h.task === task).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
|
|
3434
3568
|
return ids.length === 0 ? [] : client.fetch(
|
|
3435
3569
|
`*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
|
|
3436
|
-
{ ids,
|
|
3570
|
+
{ ids, tag }
|
|
3437
3571
|
);
|
|
3438
3572
|
},
|
|
3439
3573
|
/**
|
|
@@ -3448,11 +3582,11 @@ const workflow = {
|
|
|
3448
3582
|
}
|
|
3449
3583
|
};
|
|
3450
3584
|
async function verifyDeployedDefinitionsInternal(args) {
|
|
3451
|
-
const { client,
|
|
3452
|
-
|
|
3585
|
+
const { client, tag, effectHandlers, missingHandler, logger } = args;
|
|
3586
|
+
validateTag(tag);
|
|
3453
3587
|
const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
|
|
3454
3588
|
`*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
|
|
3455
|
-
{
|
|
3589
|
+
{ tag }
|
|
3456
3590
|
), seen = [], missingByName = /* @__PURE__ */ new Map();
|
|
3457
3591
|
for (const def of definitions)
|
|
3458
3592
|
seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
|
|
@@ -3501,7 +3635,7 @@ function walkEffectNames(def, visit) {
|
|
|
3501
3635
|
async function drainEffectsInternal(args) {
|
|
3502
3636
|
const {
|
|
3503
3637
|
client,
|
|
3504
|
-
|
|
3638
|
+
tag,
|
|
3505
3639
|
workflowResource,
|
|
3506
3640
|
resourceClients,
|
|
3507
3641
|
instanceId,
|
|
@@ -3512,10 +3646,10 @@ async function drainEffectsInternal(args) {
|
|
|
3512
3646
|
actor: drainerActor,
|
|
3513
3647
|
...args.access?.grants !== void 0 ? { grants: args.access.grants } : {}
|
|
3514
3648
|
};
|
|
3515
|
-
|
|
3649
|
+
validateTag(tag);
|
|
3516
3650
|
const log = logger("drainEffects"), drained = [], failed = [], skipped = [], skippedKeys = /* @__PURE__ */ new Set();
|
|
3517
3651
|
for (; ; ) {
|
|
3518
|
-
const before = await reload(client, instanceId,
|
|
3652
|
+
const before = await reload(client, instanceId, tag), candidate = before.pendingEffects.find(
|
|
3519
3653
|
(e) => e.claim === void 0 && !skippedKeys.has(e._key)
|
|
3520
3654
|
);
|
|
3521
3655
|
if (candidate === void 0) break;
|
|
@@ -3532,7 +3666,7 @@ async function drainEffectsInternal(args) {
|
|
|
3532
3666
|
});
|
|
3533
3667
|
await reportEffectOutcome({
|
|
3534
3668
|
client,
|
|
3535
|
-
|
|
3669
|
+
tag,
|
|
3536
3670
|
workflowResource,
|
|
3537
3671
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3538
3672
|
instanceId,
|
|
@@ -3571,7 +3705,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
|
|
|
3571
3705
|
claimedBy: drainerActor
|
|
3572
3706
|
};
|
|
3573
3707
|
try {
|
|
3574
|
-
return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
|
|
3708
|
+
return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
|
|
3575
3709
|
} catch (error) {
|
|
3576
3710
|
if (!isRevisionConflict(error)) throw error;
|
|
3577
3711
|
return !1;
|
|
@@ -3602,7 +3736,7 @@ async function applyMissingHandler(policy, info, log) {
|
|
|
3602
3736
|
}
|
|
3603
3737
|
}
|
|
3604
3738
|
function createInstanceSession(args) {
|
|
3605
|
-
const { client,
|
|
3739
|
+
const { client, tag, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
|
|
3606
3740
|
let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
|
|
3607
3741
|
const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
|
|
3608
3742
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -3663,7 +3797,7 @@ function createInstanceSession(args) {
|
|
|
3663
3797
|
return commit(async (actor, held) => {
|
|
3664
3798
|
await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
|
|
3665
3799
|
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3666
|
-
return instance = await reload(client, instance._id,
|
|
3800
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: cascaded > 0 };
|
|
3667
3801
|
});
|
|
3668
3802
|
},
|
|
3669
3803
|
fireAction({ task, action, params }) {
|
|
@@ -3680,7 +3814,7 @@ function createInstanceSession(args) {
|
|
|
3680
3814
|
...clock !== void 0 ? { clock } : {},
|
|
3681
3815
|
...params !== void 0 ? { params } : {}
|
|
3682
3816
|
}), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3683
|
-
return instance = await reload(client, instance._id,
|
|
3817
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3684
3818
|
});
|
|
3685
3819
|
}
|
|
3686
3820
|
};
|
|
@@ -3708,11 +3842,11 @@ const defaultLoggerFactory = (name) => ({
|
|
|
3708
3842
|
}
|
|
3709
3843
|
};
|
|
3710
3844
|
function createEngine(args) {
|
|
3711
|
-
const { client, workflowResource,
|
|
3712
|
-
|
|
3845
|
+
const { client, workflowResource, resourceClients, clock, tag } = args;
|
|
3846
|
+
validateTag(tag);
|
|
3713
3847
|
const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
|
|
3714
3848
|
client,
|
|
3715
|
-
|
|
3849
|
+
tag,
|
|
3716
3850
|
workflowResource,
|
|
3717
3851
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3718
3852
|
...clock !== void 0 ? { clock } : {},
|
|
@@ -3720,7 +3854,7 @@ function createEngine(args) {
|
|
|
3720
3854
|
});
|
|
3721
3855
|
return {
|
|
3722
3856
|
client,
|
|
3723
|
-
|
|
3857
|
+
tag,
|
|
3724
3858
|
workflowResource,
|
|
3725
3859
|
effectHandlers,
|
|
3726
3860
|
missingHandler,
|
|
@@ -3736,11 +3870,11 @@ function createEngine(args) {
|
|
|
3736
3870
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
3737
3871
|
abortInstance: (rest) => workflow.abortInstance(bind(rest)),
|
|
3738
3872
|
deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
|
|
3739
|
-
getInstance: ({ instanceId }) => workflow.getInstance({ client,
|
|
3740
|
-
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client,
|
|
3873
|
+
getInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }),
|
|
3874
|
+
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3741
3875
|
instance: (instanceDoc, opts) => createInstanceSession({
|
|
3742
3876
|
client,
|
|
3743
|
-
|
|
3877
|
+
tag,
|
|
3744
3878
|
instance: instanceDoc,
|
|
3745
3879
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3746
3880
|
...clock !== void 0 ? { clock } : {},
|
|
@@ -3749,45 +3883,45 @@ function createEngine(args) {
|
|
|
3749
3883
|
}),
|
|
3750
3884
|
guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
|
|
3751
3885
|
client,
|
|
3752
|
-
|
|
3886
|
+
tag,
|
|
3753
3887
|
workflowResource,
|
|
3754
3888
|
instanceId,
|
|
3755
3889
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3756
3890
|
}),
|
|
3757
3891
|
guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
|
|
3758
3892
|
client,
|
|
3759
|
-
|
|
3893
|
+
tag,
|
|
3760
3894
|
workflowResource,
|
|
3761
3895
|
definition,
|
|
3762
3896
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3763
3897
|
}),
|
|
3764
3898
|
children: ({ instanceId, task }) => workflow.children({
|
|
3765
3899
|
client,
|
|
3766
|
-
|
|
3900
|
+
tag,
|
|
3767
3901
|
workflowResource,
|
|
3768
3902
|
instanceId,
|
|
3769
3903
|
...task !== void 0 ? { task } : {}
|
|
3770
3904
|
}),
|
|
3771
3905
|
query: ({ groq, params }) => workflow.query({
|
|
3772
3906
|
client,
|
|
3773
|
-
|
|
3907
|
+
tag,
|
|
3774
3908
|
workflowResource,
|
|
3775
3909
|
groq,
|
|
3776
3910
|
...params !== void 0 ? { params } : {}
|
|
3777
3911
|
}),
|
|
3778
3912
|
queryInScope: ({ instanceId, groq, params }) => workflow.queryInScope({
|
|
3779
3913
|
client,
|
|
3780
|
-
|
|
3914
|
+
tag,
|
|
3781
3915
|
workflowResource,
|
|
3782
3916
|
instanceId,
|
|
3783
3917
|
groq,
|
|
3784
3918
|
...params !== void 0 ? { params } : {},
|
|
3785
3919
|
...clock !== void 0 ? { clock } : {}
|
|
3786
3920
|
}),
|
|
3787
|
-
listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client,
|
|
3921
|
+
listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tag, workflowResource, instanceId }),
|
|
3788
3922
|
findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
|
|
3789
3923
|
client,
|
|
3790
|
-
|
|
3924
|
+
tag,
|
|
3791
3925
|
workflowResource,
|
|
3792
3926
|
instanceId,
|
|
3793
3927
|
...claimed !== void 0 ? { claimed } : {},
|
|
@@ -3795,7 +3929,7 @@ function createEngine(args) {
|
|
|
3795
3929
|
}),
|
|
3796
3930
|
drainEffects: ({ instanceId, access }) => drainEffectsInternal({
|
|
3797
3931
|
client,
|
|
3798
|
-
|
|
3932
|
+
tag,
|
|
3799
3933
|
workflowResource,
|
|
3800
3934
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3801
3935
|
instanceId,
|
|
@@ -3806,7 +3940,7 @@ function createEngine(args) {
|
|
|
3806
3940
|
}),
|
|
3807
3941
|
verifyDeployedDefinitions: () => verifyDeployedDefinitionsInternal({
|
|
3808
3942
|
client,
|
|
3809
|
-
|
|
3943
|
+
tag,
|
|
3810
3944
|
effectHandlers,
|
|
3811
3945
|
missingHandler,
|
|
3812
3946
|
logger
|
|
@@ -3814,7 +3948,7 @@ function createEngine(args) {
|
|
|
3814
3948
|
};
|
|
3815
3949
|
}
|
|
3816
3950
|
function docIdFor(def, target) {
|
|
3817
|
-
return definitionDocId(target.workflowResource,
|
|
3951
|
+
return definitionDocId(target.workflowResource, target.tag, def.name, def.version);
|
|
3818
3952
|
}
|
|
3819
3953
|
function diffStatus(existing, expected) {
|
|
3820
3954
|
return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
|
|
@@ -3824,7 +3958,7 @@ function diffEntry(def, existingRaw, target) {
|
|
|
3824
3958
|
...def,
|
|
3825
3959
|
_id: docId,
|
|
3826
3960
|
_type: schema.WORKFLOW_DEFINITION_TYPE,
|
|
3827
|
-
|
|
3961
|
+
tag: target.tag
|
|
3828
3962
|
}, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
|
|
3829
3963
|
return {
|
|
3830
3964
|
name: def.name,
|
|
@@ -4024,6 +4158,7 @@ exports.ActionDisabledError = ActionDisabledError;
|
|
|
4024
4158
|
exports.ActionParamsInvalidError = ActionParamsInvalidError;
|
|
4025
4159
|
exports.CascadeLimitError = CascadeLimitError;
|
|
4026
4160
|
exports.ConcurrentFireActionError = ConcurrentFireActionError;
|
|
4161
|
+
exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
|
|
4027
4162
|
exports.DISPLAY = DISPLAY;
|
|
4028
4163
|
exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
|
|
4029
4164
|
exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
|
|
@@ -4037,10 +4172,8 @@ exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
|
|
|
4037
4172
|
exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
|
|
4038
4173
|
exports.abortReason = abortReason;
|
|
4039
4174
|
exports.actionVerdict = actionVerdict;
|
|
4040
|
-
exports.assigneesOf = assigneesOf;
|
|
4041
4175
|
exports.availableActions = availableActions;
|
|
4042
4176
|
exports.buildSnapshot = buildSnapshot;
|
|
4043
|
-
exports.canonicalTag = canonicalTag;
|
|
4044
4177
|
exports.compileGuard = compileGuard;
|
|
4045
4178
|
exports.computeDiffEntries = computeDiffEntries;
|
|
4046
4179
|
exports.contentReleaseName = contentReleaseName;
|
|
@@ -4070,13 +4203,13 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
|
|
|
4070
4203
|
exports.isGdr = isGdr;
|
|
4071
4204
|
exports.isTerminalStage = isTerminalStage;
|
|
4072
4205
|
exports.lakeGuardId = lakeGuardId;
|
|
4073
|
-
exports.openStage = openStage;
|
|
4074
4206
|
exports.parseGdr = parseGdr;
|
|
4075
4207
|
exports.readsRaw = readsRaw;
|
|
4076
4208
|
exports.refCanvas = refCanvas;
|
|
4077
4209
|
exports.refDashboard = refDashboard;
|
|
4078
4210
|
exports.refDataset = refDataset;
|
|
4079
4211
|
exports.refMediaLibrary = refMediaLibrary;
|
|
4212
|
+
exports.remediationsFor = remediationsFor;
|
|
4080
4213
|
exports.resolveAccess = resolveAccess;
|
|
4081
4214
|
exports.resourceFromParsed = resourceFromParsed;
|
|
4082
4215
|
exports.retractStageGuards = retractStageGuards;
|
|
@@ -4086,7 +4219,7 @@ exports.subscriptionDocument = subscriptionDocument;
|
|
|
4086
4219
|
exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
|
|
4087
4220
|
exports.tagScopeFilter = tagScopeFilter;
|
|
4088
4221
|
exports.validateDefinition = validateDefinition;
|
|
4089
|
-
exports.
|
|
4222
|
+
exports.validateTag = validateTag;
|
|
4090
4223
|
exports.verdictGuardsForInstance = verdictGuardsForInstance;
|
|
4091
4224
|
exports.wallClock = wallClock;
|
|
4092
4225
|
exports.workflow = workflow;
|