@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.js
CHANGED
|
@@ -79,6 +79,21 @@ function gdrFromResource(res, documentId) {
|
|
|
79
79
|
}
|
|
80
80
|
return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
|
|
81
81
|
}
|
|
82
|
+
function resourceFromGdrUri(uri) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parseGdr(uri);
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (parsed.scheme === "dataset")
|
|
90
|
+
return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
|
|
91
|
+
if (parsed.resourceId !== void 0)
|
|
92
|
+
return { type: parsed.scheme, id: parsed.resourceId };
|
|
93
|
+
}
|
|
94
|
+
function sameResource(a, b) {
|
|
95
|
+
return a.type === b.type && a.id === b.id;
|
|
96
|
+
}
|
|
82
97
|
function selfGdr(doc) {
|
|
83
98
|
return gdrFromResource(doc.workflowResource, doc._id);
|
|
84
99
|
}
|
|
@@ -223,6 +238,8 @@ function validateTask(v2, stageName, task) {
|
|
|
223
238
|
for (const entry of task.state ?? [])
|
|
224
239
|
v2.checkEntry(entry, `${where}.state "${entry.name}"`);
|
|
225
240
|
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`);
|
|
241
|
+
for (const [name, groq] of Object.entries(task.requirements ?? {}))
|
|
242
|
+
v2.checkCondition(groq, `${where}.requirements "${name}"`);
|
|
226
243
|
for (const [key, groq] of effectBindings(task.effects))
|
|
227
244
|
v2.tryParse(groq, `${where} effect binding "${key}"`);
|
|
228
245
|
validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
|
|
@@ -460,10 +477,14 @@ function abortReason(instance) {
|
|
|
460
477
|
)?.reason;
|
|
461
478
|
}
|
|
462
479
|
function diagnoseInputFromEvaluation(evaluation) {
|
|
480
|
+
const stage = openStage(evaluation.instance);
|
|
463
481
|
return {
|
|
464
482
|
instance: evaluation.instance,
|
|
465
483
|
tasks: evaluation.currentStage.tasks,
|
|
466
|
-
transitions: evaluation.currentStage.transitions
|
|
484
|
+
transitions: evaluation.currentStage.transitions,
|
|
485
|
+
assignees: Object.fromEntries(
|
|
486
|
+
evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
|
|
487
|
+
)
|
|
467
488
|
};
|
|
468
489
|
}
|
|
469
490
|
function openStage(instance) {
|
|
@@ -488,18 +509,34 @@ function failedTaskCause(input) {
|
|
|
488
509
|
return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
|
|
489
510
|
}
|
|
490
511
|
function waitingState(input) {
|
|
491
|
-
const active = input.tasks.find(
|
|
512
|
+
const active = input.tasks.find(
|
|
513
|
+
(t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
|
|
514
|
+
);
|
|
492
515
|
if (active !== void 0)
|
|
493
516
|
return {
|
|
494
517
|
state: "waiting",
|
|
495
518
|
task: active.task.name,
|
|
496
|
-
assignees:
|
|
519
|
+
assignees: input.assignees[active.task.name] ?? [],
|
|
497
520
|
actions: (active.task.actions ?? []).map((a) => a.name)
|
|
498
521
|
};
|
|
499
522
|
}
|
|
523
|
+
function blockedState(input) {
|
|
524
|
+
const blocked = input.tasks.find(
|
|
525
|
+
(t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
|
|
526
|
+
);
|
|
527
|
+
if (blocked !== void 0)
|
|
528
|
+
return {
|
|
529
|
+
state: "blocked",
|
|
530
|
+
task: blocked.task.name,
|
|
531
|
+
requirements: blocked.unmetRequirements ?? [],
|
|
532
|
+
assignees: input.assignees[blocked.task.name] ?? []
|
|
533
|
+
};
|
|
534
|
+
}
|
|
500
535
|
function noTransitionFiresCause(input) {
|
|
501
|
-
if (input.transitions.length
|
|
502
|
-
return
|
|
536
|
+
if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
|
|
537
|
+
return;
|
|
538
|
+
const undecidable = input.transitions.filter((t) => t.unevaluable);
|
|
539
|
+
return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
|
|
503
540
|
}
|
|
504
541
|
function diagnoseInstance(input) {
|
|
505
542
|
const { instance } = input;
|
|
@@ -510,7 +547,51 @@ function diagnoseInstance(input) {
|
|
|
510
547
|
if (instance.completedAt !== void 0)
|
|
511
548
|
return { state: "completed", at: instance.completedAt };
|
|
512
549
|
const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
|
|
513
|
-
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
|
|
550
|
+
return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
|
|
551
|
+
}
|
|
552
|
+
const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
|
|
553
|
+
function remediationsForCause(cause) {
|
|
554
|
+
switch (cause.kind) {
|
|
555
|
+
case "failed-effect":
|
|
556
|
+
return [
|
|
557
|
+
{
|
|
558
|
+
verb: "retry-effect",
|
|
559
|
+
rationale: "Re-run the failed effect once the upstream system is healthy."
|
|
560
|
+
},
|
|
561
|
+
{ verb: "abort", rationale: "Abort the instance if it can no longer recover." }
|
|
562
|
+
];
|
|
563
|
+
case "hung-effect":
|
|
564
|
+
return [
|
|
565
|
+
{
|
|
566
|
+
verb: "drain-effects",
|
|
567
|
+
rationale: "Re-run the effect drainer to re-pick the claimed effect."
|
|
568
|
+
},
|
|
569
|
+
{ verb: "abort", rationale: "Abort the instance if the effect never drains." }
|
|
570
|
+
];
|
|
571
|
+
case "failed-task":
|
|
572
|
+
return [
|
|
573
|
+
{ verb: "reset-task", rationale: "Reset or skip the failed task." },
|
|
574
|
+
{
|
|
575
|
+
verb: "set-stage",
|
|
576
|
+
rationale: "Move the instance past the failed task to the intended next stage."
|
|
577
|
+
}
|
|
578
|
+
];
|
|
579
|
+
case "no-transition-fires":
|
|
580
|
+
return [
|
|
581
|
+
{
|
|
582
|
+
verb: "set-stage",
|
|
583
|
+
rationale: "Move the instance to the intended next stage to force it forward."
|
|
584
|
+
}
|
|
585
|
+
];
|
|
586
|
+
case "transition-unevaluable":
|
|
587
|
+
return [];
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function remediationsFor(diagnosis) {
|
|
591
|
+
return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
|
|
592
|
+
...seed,
|
|
593
|
+
available: RUNNABLE_VERBS.has(seed.verb)
|
|
594
|
+
}));
|
|
514
595
|
}
|
|
515
596
|
function buildSnapshot(args) {
|
|
516
597
|
const docs = [], knownIds = /* @__PURE__ */ new Set();
|
|
@@ -557,6 +638,7 @@ function collectWatchRefs(instance) {
|
|
|
557
638
|
function readsRaw(ref) {
|
|
558
639
|
return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
|
|
559
640
|
}
|
|
641
|
+
const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
|
|
560
642
|
function contentReleaseName(args) {
|
|
561
643
|
const { ref, perspective } = args;
|
|
562
644
|
if (!readsRaw(ref) && Array.isArray(perspective))
|
|
@@ -582,6 +664,11 @@ function entryReleaseRefs(state) {
|
|
|
582
664
|
(entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
|
|
583
665
|
);
|
|
584
666
|
}
|
|
667
|
+
function entrySubjectRefs(state) {
|
|
668
|
+
return stateEntries(state).flatMap(
|
|
669
|
+
(entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
|
|
670
|
+
);
|
|
671
|
+
}
|
|
585
672
|
function stateEntries(state) {
|
|
586
673
|
return Array.isArray(state) ? state.filter(
|
|
587
674
|
(entry) => !!entry && typeof entry == "object"
|
|
@@ -606,7 +693,10 @@ async function hydrateSnapshot(args) {
|
|
|
606
693
|
};
|
|
607
694
|
loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
|
|
608
695
|
for (const ref of collectWatchRefs(instance))
|
|
609
|
-
await loadInto(
|
|
696
|
+
await loadInto(
|
|
697
|
+
ref.id,
|
|
698
|
+
readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
|
|
699
|
+
);
|
|
610
700
|
return buildSnapshot({ docs: loaded });
|
|
611
701
|
}
|
|
612
702
|
async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
|
|
@@ -621,7 +711,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
|
|
|
621
711
|
return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
|
|
622
712
|
}
|
|
623
713
|
async function readDoc(client, id, perspective) {
|
|
624
|
-
return perspective ===
|
|
714
|
+
return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
|
|
625
715
|
}
|
|
626
716
|
function resourceFromParsed(parsed) {
|
|
627
717
|
return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
|
|
@@ -722,23 +812,7 @@ async function queryGuardsAcross(clients, query, params) {
|
|
|
722
812
|
function dedupById(guards) {
|
|
723
813
|
return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
|
|
724
814
|
}
|
|
725
|
-
const
|
|
726
|
-
function validateTags(tags) {
|
|
727
|
-
if (tags.length === 0)
|
|
728
|
-
throw new Error("tags: must be a non-empty array");
|
|
729
|
-
for (const tag of tags)
|
|
730
|
-
if (!TAG_RE.test(tag))
|
|
731
|
-
throw new Error(
|
|
732
|
-
`tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
function canonicalTag(tags) {
|
|
736
|
-
return tags[0];
|
|
737
|
-
}
|
|
738
|
-
function tagScopeFilter(param = "engineTags") {
|
|
739
|
-
return `count(tags[@ in $${param}]) > 0`;
|
|
740
|
-
}
|
|
741
|
-
const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
815
|
+
const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
742
816
|
function resolveGuard(guard, instance, stageName, now) {
|
|
743
817
|
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
744
818
|
if (targets === null) return null;
|
|
@@ -765,11 +839,11 @@ function resolveGuard(guard, instance, stageName, now) {
|
|
|
765
839
|
}
|
|
766
840
|
async function upsertGuard(client, doc) {
|
|
767
841
|
if (!await client.getDocument(doc._id)) {
|
|
768
|
-
await client.create(doc);
|
|
842
|
+
await client.create(doc, SYNC_COMMIT);
|
|
769
843
|
return;
|
|
770
844
|
}
|
|
771
845
|
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
772
|
-
await client.patch(doc._id).set(body).commit();
|
|
846
|
+
await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
|
|
773
847
|
}
|
|
774
848
|
function resolvedStageGuards(args) {
|
|
775
849
|
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
@@ -792,16 +866,16 @@ async function retractStageGuards(args) {
|
|
|
792
866
|
const live = await committedInstance(args);
|
|
793
867
|
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
794
868
|
for (const { client, doc } of resolvedStageGuards(args))
|
|
795
|
-
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
|
|
869
|
+
await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
|
|
796
870
|
}
|
|
797
871
|
async function deleteOrphanedDefinitionGuards(args) {
|
|
798
|
-
const { client, clientForGdr, workflowResource, definition, definitions,
|
|
872
|
+
const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
|
|
799
873
|
client,
|
|
800
874
|
clientForGdr,
|
|
801
875
|
workflowResource,
|
|
802
876
|
definition,
|
|
803
877
|
definitions
|
|
804
|
-
}), ownPartitionPrefix = `${
|
|
878
|
+
}), ownPartitionPrefix = `${tag}.`;
|
|
805
879
|
let count = 0;
|
|
806
880
|
for (const { client: resourceClient, guards } of perClient) {
|
|
807
881
|
const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
|
|
@@ -816,16 +890,32 @@ function randomKey(length = 12) {
|
|
|
816
890
|
const bytes = new Uint8Array(length);
|
|
817
891
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
818
892
|
}
|
|
819
|
-
|
|
893
|
+
function isUnevaluable(result) {
|
|
894
|
+
return result == null;
|
|
895
|
+
}
|
|
896
|
+
async function evaluateConditionOutcome(args) {
|
|
820
897
|
const { condition, snapshot, params } = args;
|
|
821
|
-
|
|
898
|
+
if (condition === void 0) return "satisfied";
|
|
899
|
+
const result = await runGroq(condition, params, snapshot);
|
|
900
|
+
return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
|
|
901
|
+
}
|
|
902
|
+
async function evaluateCondition(args) {
|
|
903
|
+
return await evaluateConditionOutcome(args) === "satisfied";
|
|
822
904
|
}
|
|
823
905
|
async function evaluatePredicates(args) {
|
|
824
906
|
const out = {};
|
|
825
|
-
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
826
|
-
|
|
907
|
+
for (const [name, groq] of Object.entries(args.predicates ?? {})) {
|
|
908
|
+
const result = await runGroq(groq, args.params, args.snapshot);
|
|
909
|
+
out[name] = isUnevaluable(result) ? null : !!result;
|
|
910
|
+
}
|
|
827
911
|
return out;
|
|
828
912
|
}
|
|
913
|
+
async function evaluateRequirements(args) {
|
|
914
|
+
const unmet = [];
|
|
915
|
+
for (const [name, condition] of Object.entries(args.requirements ?? {}))
|
|
916
|
+
await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
|
|
917
|
+
return unmet;
|
|
918
|
+
}
|
|
829
919
|
async function runGroq(groq, params, snapshot) {
|
|
830
920
|
const tree = parse(groq, { params });
|
|
831
921
|
return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
@@ -990,10 +1080,10 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
|
|
|
990
1080
|
stage: ctx.stageName ?? null,
|
|
991
1081
|
task: ctx.taskName ?? null,
|
|
992
1082
|
now: ctx.now,
|
|
993
|
-
|
|
1083
|
+
tag: ctx.tag
|
|
994
1084
|
});
|
|
995
1085
|
try {
|
|
996
|
-
const fetchOptions =
|
|
1086
|
+
const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
|
|
997
1087
|
return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
|
|
998
1088
|
} catch (err) {
|
|
999
1089
|
throw new Error(
|
|
@@ -1123,13 +1213,16 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1123
1213
|
params
|
|
1124
1214
|
}), ...params };
|
|
1125
1215
|
}
|
|
1126
|
-
async function
|
|
1127
|
-
return condition === void 0 ?
|
|
1216
|
+
async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
|
|
1217
|
+
return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
|
|
1128
1218
|
condition,
|
|
1129
1219
|
snapshot: ctx.snapshot,
|
|
1130
1220
|
params: await ctxConditionParams(ctx, opts)
|
|
1131
1221
|
});
|
|
1132
1222
|
}
|
|
1223
|
+
async function ctxEvaluateCondition(ctx, condition, opts) {
|
|
1224
|
+
return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
|
|
1225
|
+
}
|
|
1133
1226
|
async function buildEngineContext(args) {
|
|
1134
1227
|
const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
|
|
1135
1228
|
return {
|
|
@@ -1157,7 +1250,7 @@ async function resolveStageStateEntries(args) {
|
|
|
1157
1250
|
client,
|
|
1158
1251
|
now,
|
|
1159
1252
|
selfId: instance._id,
|
|
1160
|
-
|
|
1253
|
+
tag: instance.tag,
|
|
1161
1254
|
stageName: stage.name,
|
|
1162
1255
|
workflowResource: instance.workflowResource,
|
|
1163
1256
|
// Allow stage-scope entries' `source: { type: "stateRead", scope:
|
|
@@ -1177,7 +1270,7 @@ async function resolveTaskStateEntries(args) {
|
|
|
1177
1270
|
client,
|
|
1178
1271
|
now,
|
|
1179
1272
|
selfId: instance._id,
|
|
1180
|
-
|
|
1273
|
+
tag: instance.tag,
|
|
1181
1274
|
stageName: stage.name,
|
|
1182
1275
|
workflowResource: instance.workflowResource,
|
|
1183
1276
|
taskName: task.name,
|
|
@@ -1249,7 +1342,7 @@ async function deployOrRollback(args) {
|
|
|
1249
1342
|
});
|
|
1250
1343
|
try {
|
|
1251
1344
|
let patch = args.client.patch(args.instanceId).set(args.restore);
|
|
1252
|
-
args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
|
|
1345
|
+
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);
|
|
1253
1346
|
} catch (rollbackError) {
|
|
1254
1347
|
throw new WorkflowStateDivergedError({
|
|
1255
1348
|
instanceId: args.instanceId,
|
|
@@ -1356,7 +1449,7 @@ async function persist(ctx, mutation) {
|
|
|
1356
1449
|
lastChangedAt: ctx.now
|
|
1357
1450
|
}, pendingCreates = mutation.pendingCreates;
|
|
1358
1451
|
if (pendingCreates.length === 0)
|
|
1359
|
-
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
|
|
1452
|
+
return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
|
|
1360
1453
|
const tx = ctx.client.transaction();
|
|
1361
1454
|
for (const body of pendingCreates) tx.create(body);
|
|
1362
1455
|
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
@@ -1399,6 +1492,16 @@ function findCurrentStageEntry(instance) {
|
|
|
1399
1492
|
function findCurrentTasks(instance) {
|
|
1400
1493
|
return findCurrentStageEntry(instance)?.tasks ?? [];
|
|
1401
1494
|
}
|
|
1495
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
1496
|
+
function validateTag(tag) {
|
|
1497
|
+
if (!TAG_RE.test(tag))
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`tag: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
function tagScopeFilter() {
|
|
1503
|
+
return "tag == $tag";
|
|
1504
|
+
}
|
|
1402
1505
|
function definitionLookupGroq(explicit) {
|
|
1403
1506
|
const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
|
|
1404
1507
|
return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
|
|
@@ -1415,6 +1518,14 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
|
|
|
1415
1518
|
return defaultClient;
|
|
1416
1519
|
}
|
|
1417
1520
|
}
|
|
1521
|
+
async function reload(client, instanceId, tag) {
|
|
1522
|
+
const doc = await client.getDocument(instanceId);
|
|
1523
|
+
if (!doc)
|
|
1524
|
+
throw new Error(`Workflow instance ${instanceId} not found`);
|
|
1525
|
+
if (doc.tag !== tag)
|
|
1526
|
+
throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
|
|
1527
|
+
return doc;
|
|
1528
|
+
}
|
|
1418
1529
|
function effectsContextEntry(name, value) {
|
|
1419
1530
|
const _key = randomKey();
|
|
1420
1531
|
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 };
|
|
@@ -1430,7 +1541,7 @@ function buildInstanceBase(args) {
|
|
|
1430
1541
|
_rev: "",
|
|
1431
1542
|
_createdAt: now,
|
|
1432
1543
|
_updatedAt: now,
|
|
1433
|
-
|
|
1544
|
+
tag: args.tag,
|
|
1434
1545
|
workflowResource: args.workflowResource,
|
|
1435
1546
|
definition: args.definitionName,
|
|
1436
1547
|
pinnedVersion: args.pinnedVersion,
|
|
@@ -1481,7 +1592,7 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1481
1592
|
`Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
|
|
1482
1593
|
);
|
|
1483
1594
|
}
|
|
1484
|
-
const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.
|
|
1595
|
+
const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tag);
|
|
1485
1596
|
if (definition === null) {
|
|
1486
1597
|
const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
|
|
1487
1598
|
throw new Error(
|
|
@@ -1491,9 +1602,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1491
1602
|
const parentScope = await ctxConditionParams(ctx, {
|
|
1492
1603
|
taskName: task.name,
|
|
1493
1604
|
...actor ? { actor } : {}
|
|
1494
|
-
}), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
|
|
1605
|
+
}), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
|
|
1495
1606
|
for (const row of rows) {
|
|
1496
|
-
const initialState = await projectRowState(
|
|
1607
|
+
const initialState = await projectRowState(
|
|
1608
|
+
ctx,
|
|
1609
|
+
sub,
|
|
1610
|
+
definition,
|
|
1611
|
+
parentScope,
|
|
1612
|
+
row,
|
|
1613
|
+
discoveryResource
|
|
1614
|
+
), { ref, body } = await prepareChildInstance({
|
|
1497
1615
|
client: ctx.client,
|
|
1498
1616
|
parent: ctx.instance,
|
|
1499
1617
|
definition,
|
|
@@ -1508,28 +1626,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1508
1626
|
}
|
|
1509
1627
|
async function resolveSpawnRows(ctx, sub) {
|
|
1510
1628
|
const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
|
|
1511
|
-
let value;
|
|
1629
|
+
let value, discoveryResource;
|
|
1512
1630
|
if (groq.includes("*")) {
|
|
1513
|
-
const routingUri =
|
|
1514
|
-
value = await discoverItems({
|
|
1631
|
+
const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
|
|
1632
|
+
client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
|
|
1515
1633
|
client,
|
|
1516
1634
|
groq,
|
|
1517
1635
|
params,
|
|
1518
|
-
|
|
1636
|
+
// Draft-aware by default (drafts overlay published) when there's no
|
|
1637
|
+
// release, so a `forEach` discovers in-flight items the same way the
|
|
1638
|
+
// engine reads any other content.
|
|
1639
|
+
perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
|
|
1519
1640
|
});
|
|
1520
1641
|
} else {
|
|
1521
1642
|
const tree = parse(groq, { params });
|
|
1522
1643
|
value = await (await evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
|
|
1523
1644
|
}
|
|
1524
|
-
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
1645
|
+
return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
|
|
1525
1646
|
}
|
|
1526
|
-
function
|
|
1527
|
-
if (entries) {
|
|
1528
|
-
for (const s of entries)
|
|
1529
|
-
if (s._type === "doc.ref" && s.value !== null) return s.value.id;
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
1647
|
+
async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
|
|
1533
1648
|
const out = [];
|
|
1534
1649
|
for (const [name, groq] of Object.entries(sub.with ?? {})) {
|
|
1535
1650
|
const declared = (childDefinition.state ?? []).find((e) => e.name === name);
|
|
@@ -1537,7 +1652,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
|
1537
1652
|
throw new Error(
|
|
1538
1653
|
`subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
|
|
1539
1654
|
);
|
|
1540
|
-
const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw,
|
|
1655
|
+
const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
|
|
1656
|
+
parentResource: ctx.instance.workflowResource,
|
|
1657
|
+
discoveryResource,
|
|
1658
|
+
entryName: name,
|
|
1659
|
+
childName: childDefinition.name
|
|
1660
|
+
});
|
|
1541
1661
|
value != null && out.push({
|
|
1542
1662
|
type: declared.type,
|
|
1543
1663
|
name,
|
|
@@ -1554,11 +1674,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
|
|
|
1554
1674
|
}
|
|
1555
1675
|
return out;
|
|
1556
1676
|
}
|
|
1557
|
-
function canonicaliseSpawnValue(kind, value,
|
|
1558
|
-
return kind === "doc.ref" ? coerceSpawnGdr(value,
|
|
1677
|
+
function canonicaliseSpawnValue(kind, value, cx) {
|
|
1678
|
+
return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
|
|
1679
|
+
}
|
|
1680
|
+
function assertBareIdRootsCorrectly(id, cx) {
|
|
1681
|
+
if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
|
|
1682
|
+
throw new Error(
|
|
1683
|
+
`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.`
|
|
1684
|
+
);
|
|
1559
1685
|
}
|
|
1560
|
-
function coerceSpawnGdr(raw,
|
|
1561
|
-
const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(
|
|
1686
|
+
function coerceSpawnGdr(raw, cx) {
|
|
1687
|
+
const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
|
|
1562
1688
|
if (raw == null) return null;
|
|
1563
1689
|
if (typeof raw == "object" && "id" in raw && "type" in raw) {
|
|
1564
1690
|
const r = raw;
|
|
@@ -1571,15 +1697,15 @@ function coerceSpawnGdr(raw, workflowResource) {
|
|
|
1571
1697
|
}
|
|
1572
1698
|
return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
|
|
1573
1699
|
}
|
|
1574
|
-
async function resolveDefinitionRef(client, ref,
|
|
1575
|
-
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name,
|
|
1700
|
+
async function resolveDefinitionRef(client, ref, tag) {
|
|
1701
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
|
|
1576
1702
|
return wantsExplicit && (params.version = ref.version), await client.fetch(
|
|
1577
1703
|
definitionLookupGroq(wantsExplicit),
|
|
1578
1704
|
params
|
|
1579
1705
|
) ?? null;
|
|
1580
1706
|
}
|
|
1581
1707
|
async function prepareChildInstance(args) {
|
|
1582
|
-
const { client, parent, definition, initialState, effectsContext, actor, now } = args,
|
|
1708
|
+
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 = [
|
|
1583
1709
|
...parent.ancestors,
|
|
1584
1710
|
{
|
|
1585
1711
|
id: selfGdr(parent),
|
|
@@ -1592,7 +1718,7 @@ async function prepareChildInstance(args) {
|
|
|
1592
1718
|
client,
|
|
1593
1719
|
now,
|
|
1594
1720
|
selfId: childDocId,
|
|
1595
|
-
|
|
1721
|
+
tag: childTag,
|
|
1596
1722
|
workflowResource,
|
|
1597
1723
|
...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
|
|
1598
1724
|
},
|
|
@@ -1611,7 +1737,7 @@ async function prepareChildInstance(args) {
|
|
|
1611
1737
|
), base = buildInstanceBase({
|
|
1612
1738
|
id: childDocId,
|
|
1613
1739
|
now,
|
|
1614
|
-
|
|
1740
|
+
tag: childTag,
|
|
1615
1741
|
workflowResource,
|
|
1616
1742
|
definitionName: definition.name,
|
|
1617
1743
|
pinnedVersion: definition.version,
|
|
@@ -2057,21 +2183,24 @@ function resolveMachineStep(mutation, task, entry, now) {
|
|
|
2057
2183
|
async function buildStageTasks(ctx, stage) {
|
|
2058
2184
|
const entries = [];
|
|
2059
2185
|
for (const task of stage.tasks ?? []) {
|
|
2060
|
-
const
|
|
2186
|
+
const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
|
|
2061
2187
|
entries.push({
|
|
2062
2188
|
_key: randomKey(),
|
|
2063
2189
|
name: task.name,
|
|
2064
2190
|
status: inScope ? "pending" : "skipped",
|
|
2065
|
-
...task.filter !== void 0 ? {
|
|
2066
|
-
filterEvaluation: {
|
|
2067
|
-
at: ctx.now,
|
|
2068
|
-
truthy: inScope
|
|
2069
|
-
}
|
|
2070
|
-
} : {}
|
|
2191
|
+
...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
|
|
2071
2192
|
});
|
|
2072
2193
|
}
|
|
2073
2194
|
return entries;
|
|
2074
2195
|
}
|
|
2196
|
+
function buildFilterEvaluation(at, filter, outcome) {
|
|
2197
|
+
return outcome === "unevaluable" ? {
|
|
2198
|
+
at,
|
|
2199
|
+
truthy: !1,
|
|
2200
|
+
unevaluable: !0,
|
|
2201
|
+
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.`
|
|
2202
|
+
} : { at, truthy: outcome === "satisfied" };
|
|
2203
|
+
}
|
|
2075
2204
|
function isTerminalStage(stage) {
|
|
2076
2205
|
return (stage.transitions ?? []).length === 0;
|
|
2077
2206
|
}
|
|
@@ -2221,8 +2350,11 @@ async function commitTransition(ctx, actor) {
|
|
|
2221
2350
|
};
|
|
2222
2351
|
}
|
|
2223
2352
|
async function pickTransition(ctx, stage) {
|
|
2224
|
-
for (const candidate of stage.transitions ?? [])
|
|
2225
|
-
|
|
2353
|
+
for (const candidate of stage.transitions ?? []) {
|
|
2354
|
+
const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
|
|
2355
|
+
if (outcome === "satisfied") return candidate;
|
|
2356
|
+
if (outcome === "unevaluable") return;
|
|
2357
|
+
}
|
|
2226
2358
|
}
|
|
2227
2359
|
async function evaluateAutoTransitions(args) {
|
|
2228
2360
|
const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
|
|
@@ -2252,7 +2384,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
2252
2384
|
}, committed = await client.patch(instance._id).set({
|
|
2253
2385
|
stages: [initialStageEntry],
|
|
2254
2386
|
lastChangedAt: now
|
|
2255
|
-
}).ifRevisionId(instance._rev).commit();
|
|
2387
|
+
}).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
|
|
2256
2388
|
await deployOrRollback({
|
|
2257
2389
|
client,
|
|
2258
2390
|
instanceId: instance._id,
|
|
@@ -2477,17 +2609,12 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2477
2609
|
}
|
|
2478
2610
|
}
|
|
2479
2611
|
async function evaluateInstance(args) {
|
|
2480
|
-
const { client,
|
|
2481
|
-
|
|
2612
|
+
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2613
|
+
validateTag(tag);
|
|
2482
2614
|
const { actor, grants } = await resolveAccess(client, {
|
|
2483
2615
|
...args.access !== void 0 ? { override: args.access } : {},
|
|
2484
2616
|
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
2485
|
-
}), instance = await client
|
|
2486
|
-
if (!instance)
|
|
2487
|
-
throw new Error(`Workflow instance "${instanceId}" not found`);
|
|
2488
|
-
if (!tags.some((t) => instance.tags?.includes(t)))
|
|
2489
|
-
throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
|
|
2490
|
-
const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
|
|
2617
|
+
}), 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);
|
|
2491
2618
|
return evaluateFromSnapshot({
|
|
2492
2619
|
instance,
|
|
2493
2620
|
definition,
|
|
@@ -2519,15 +2646,18 @@ async function evaluateFromSnapshot(args) {
|
|
|
2519
2646
|
})
|
|
2520
2647
|
);
|
|
2521
2648
|
const transitionEvaluations = [];
|
|
2522
|
-
for (const transition of stage.transitions ?? [])
|
|
2649
|
+
for (const transition of stage.transitions ?? []) {
|
|
2650
|
+
const outcome = await evaluateConditionOutcome({
|
|
2651
|
+
condition: transition.filter,
|
|
2652
|
+
snapshot,
|
|
2653
|
+
params: scope
|
|
2654
|
+
});
|
|
2523
2655
|
transitionEvaluations.push({
|
|
2524
2656
|
transition,
|
|
2525
|
-
filterSatisfied:
|
|
2526
|
-
|
|
2527
|
-
snapshot,
|
|
2528
|
-
params: scope
|
|
2529
|
-
})
|
|
2657
|
+
filterSatisfied: outcome === "satisfied",
|
|
2658
|
+
unevaluable: outcome === "unevaluable"
|
|
2530
2659
|
});
|
|
2660
|
+
}
|
|
2531
2661
|
const currentStage = {
|
|
2532
2662
|
stage,
|
|
2533
2663
|
tasks: taskEvaluations,
|
|
@@ -2575,7 +2705,11 @@ async function evaluateTask(args) {
|
|
|
2575
2705
|
...scopedStateOverlay(instance, snapshot, task.name)
|
|
2576
2706
|
},
|
|
2577
2707
|
assigned
|
|
2578
|
-
},
|
|
2708
|
+
}, unmetRequirements = await evaluateRequirements({
|
|
2709
|
+
requirements: task.requirements,
|
|
2710
|
+
snapshot,
|
|
2711
|
+
params: taskScope
|
|
2712
|
+
}), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
|
|
2579
2713
|
for (const action of task.actions ?? [])
|
|
2580
2714
|
actions.push(
|
|
2581
2715
|
await evaluateAction({
|
|
@@ -2585,7 +2719,8 @@ async function evaluateTask(args) {
|
|
|
2585
2719
|
snapshot,
|
|
2586
2720
|
taskScope,
|
|
2587
2721
|
stageHasExits,
|
|
2588
|
-
guardDenial
|
|
2722
|
+
guardDenial,
|
|
2723
|
+
requirementsReason
|
|
2589
2724
|
})
|
|
2590
2725
|
);
|
|
2591
2726
|
return {
|
|
@@ -2596,12 +2731,22 @@ async function evaluateTask(args) {
|
|
|
2596
2731
|
// they're still inbox items. The inbox reads the assignees-kind state
|
|
2597
2732
|
// entry BY KIND (who-data is state).
|
|
2598
2733
|
pendingOnActor: (status === "active" || status === "pending") && assigned,
|
|
2734
|
+
...unmetRequirements.length > 0 ? { unmetRequirements } : {},
|
|
2599
2735
|
actions
|
|
2600
2736
|
};
|
|
2601
2737
|
}
|
|
2602
2738
|
async function evaluateAction(args) {
|
|
2603
|
-
const {
|
|
2604
|
-
|
|
2739
|
+
const {
|
|
2740
|
+
action,
|
|
2741
|
+
status,
|
|
2742
|
+
instance,
|
|
2743
|
+
snapshot,
|
|
2744
|
+
taskScope,
|
|
2745
|
+
stageHasExits,
|
|
2746
|
+
guardDenial,
|
|
2747
|
+
requirementsReason
|
|
2748
|
+
} = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
|
|
2749
|
+
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({
|
|
2605
2750
|
condition: action.filter,
|
|
2606
2751
|
snapshot,
|
|
2607
2752
|
params: taskScope
|
|
@@ -2628,22 +2773,21 @@ function lifecycleReason(instance, status, stageHasExits) {
|
|
|
2628
2773
|
function disabled(action, reason) {
|
|
2629
2774
|
return { action, allowed: !1, disabledReason: reason };
|
|
2630
2775
|
}
|
|
2631
|
-
function definitionDocId(_workflowResource,
|
|
2632
|
-
return `${
|
|
2776
|
+
function definitionDocId(_workflowResource, tag, definition, version) {
|
|
2777
|
+
return `${tag}.${definition}.v${version}`;
|
|
2633
2778
|
}
|
|
2634
|
-
async function sortByDependencies(client, definitions,
|
|
2635
|
-
const
|
|
2779
|
+
async function sortByDependencies(client, definitions, tag, workflowResource) {
|
|
2780
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2636
2781
|
for (const def of definitions) {
|
|
2637
2782
|
const list = byName.get(def.name) ?? [];
|
|
2638
2783
|
list.push(def), byName.set(def.name, list);
|
|
2639
2784
|
}
|
|
2640
2785
|
const findInBatch = (ref) => findRefInBatch(byName, ref);
|
|
2641
2786
|
return await assertCrossBatchRefsDeployed(client, definitions, {
|
|
2642
|
-
|
|
2787
|
+
tag,
|
|
2643
2788
|
workflowResource,
|
|
2644
|
-
prefix,
|
|
2645
2789
|
findInBatch
|
|
2646
|
-
}), topoSortDefinitions(definitions, { workflowResource,
|
|
2790
|
+
}), topoSortDefinitions(definitions, { workflowResource, tag, findInBatch });
|
|
2647
2791
|
}
|
|
2648
2792
|
function findRefInBatch(byName, ref) {
|
|
2649
2793
|
const candidates = byName.get(ref.name);
|
|
@@ -2656,10 +2800,10 @@ function findRefInBatch(byName, ref) {
|
|
|
2656
2800
|
async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
2657
2801
|
const missing = [];
|
|
2658
2802
|
for (const def of definitions) {
|
|
2659
|
-
const fromId = definitionDocId(ctx.workflowResource, ctx.
|
|
2803
|
+
const fromId = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
|
|
2660
2804
|
for (const ref of refsOf(def)) {
|
|
2661
2805
|
if (ctx.findInBatch(ref) !== void 0) continue;
|
|
2662
|
-
const label = await resolveDeployedRefLabel(client, ref, ctx.
|
|
2806
|
+
const label = await resolveDeployedRefLabel(client, ref, ctx.tag);
|
|
2663
2807
|
label !== void 0 && missing.push({ from: fromId, ref: label });
|
|
2664
2808
|
}
|
|
2665
2809
|
}
|
|
@@ -2671,8 +2815,8 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
|
|
|
2671
2815
|
`)
|
|
2672
2816
|
);
|
|
2673
2817
|
}
|
|
2674
|
-
async function resolveDeployedRefLabel(client, ref,
|
|
2675
|
-
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name,
|
|
2818
|
+
async function resolveDeployedRefLabel(client, ref, tag) {
|
|
2819
|
+
const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
|
|
2676
2820
|
if (wantsExplicit && (params.version = ref.version), !await client.fetch(
|
|
2677
2821
|
definitionLookupGroq(wantsExplicit),
|
|
2678
2822
|
params
|
|
@@ -2681,7 +2825,7 @@ async function resolveDeployedRefLabel(client, ref, tags) {
|
|
|
2681
2825
|
}
|
|
2682
2826
|
function topoSortDefinitions(definitions, ctx) {
|
|
2683
2827
|
const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
|
|
2684
|
-
const id = definitionDocId(ctx.workflowResource, ctx.
|
|
2828
|
+
const id = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
|
|
2685
2829
|
if (!visited.has(id)) {
|
|
2686
2830
|
if (visiting.has(id))
|
|
2687
2831
|
throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
|
|
@@ -2722,11 +2866,11 @@ function stableStringify(value) {
|
|
|
2722
2866
|
([a], [b]) => a.localeCompare(b)
|
|
2723
2867
|
).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
|
|
2724
2868
|
}
|
|
2725
|
-
async function loadDefinition(client, definition, version,
|
|
2869
|
+
async function loadDefinition(client, definition, version, tag) {
|
|
2726
2870
|
if (version !== void 0) {
|
|
2727
2871
|
const doc = await client.fetch(
|
|
2728
2872
|
definitionLookupGroq(!0),
|
|
2729
|
-
{ definition, version,
|
|
2873
|
+
{ definition, version, tag }
|
|
2730
2874
|
);
|
|
2731
2875
|
if (!doc)
|
|
2732
2876
|
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
@@ -2734,16 +2878,16 @@ async function loadDefinition(client, definition, version, tags) {
|
|
|
2734
2878
|
}
|
|
2735
2879
|
const latest = await client.fetch(definitionLookupGroq(!1), {
|
|
2736
2880
|
definition,
|
|
2737
|
-
|
|
2881
|
+
tag
|
|
2738
2882
|
});
|
|
2739
2883
|
if (!latest)
|
|
2740
2884
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2741
2885
|
return latest;
|
|
2742
2886
|
}
|
|
2743
|
-
async function loadDefinitionVersions(client, definition,
|
|
2887
|
+
async function loadDefinitionVersions(client, definition, tag) {
|
|
2744
2888
|
return client.fetch(
|
|
2745
|
-
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition &&
|
|
2746
|
-
{ definition,
|
|
2889
|
+
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}] | order(version desc)`,
|
|
2890
|
+
{ definition, tag }
|
|
2747
2891
|
);
|
|
2748
2892
|
}
|
|
2749
2893
|
async function abortInstance(args) {
|
|
@@ -2879,7 +3023,8 @@ const disabledReasonDetail = {
|
|
|
2879
3023
|
"task-not-active": (r) => `task status is "${r.status}"`,
|
|
2880
3024
|
"stage-terminal": (r) => `stage "${r.stage}" is terminal`,
|
|
2881
3025
|
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
2882
|
-
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}
|
|
3026
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
|
|
3027
|
+
"requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
|
|
2883
3028
|
};
|
|
2884
3029
|
function formatDisabledReason(task, action, reason) {
|
|
2885
3030
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
@@ -2894,7 +3039,7 @@ function bareIdFromSpawnRef(uri) {
|
|
|
2894
3039
|
}
|
|
2895
3040
|
}
|
|
2896
3041
|
async function resolveOperationContext(args) {
|
|
2897
|
-
|
|
3042
|
+
validateTag(args.tag);
|
|
2898
3043
|
const access = await resolveAccess(args.client, {
|
|
2899
3044
|
...args.access !== void 0 ? { override: args.access } : {},
|
|
2900
3045
|
...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
|
|
@@ -2928,16 +3073,6 @@ async function abortAndPropagate(args) {
|
|
|
2928
3073
|
function buildClientForGdr(defaultClient, resolver) {
|
|
2929
3074
|
return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
|
|
2930
3075
|
}
|
|
2931
|
-
async function reload(client, instanceId, tags) {
|
|
2932
|
-
const doc = await client.getDocument(instanceId);
|
|
2933
|
-
if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
|
|
2934
|
-
if (!intersectsTags(doc.tags, tags))
|
|
2935
|
-
throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
|
|
2936
|
-
return doc;
|
|
2937
|
-
}
|
|
2938
|
-
function intersectsTags(docTags, engineTags) {
|
|
2939
|
-
return !docTags || docTags.length === 0 ? !1 : engineTags.some((t) => docTags.includes(t));
|
|
2940
|
-
}
|
|
2941
3076
|
function toEffectsContextEntries(ctx) {
|
|
2942
3077
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
2943
3078
|
}
|
|
@@ -2963,15 +3098,15 @@ async function applyAction(args) {
|
|
|
2963
3098
|
})).ranOps;
|
|
2964
3099
|
}
|
|
2965
3100
|
async function deleteDefinition(args) {
|
|
2966
|
-
const { client,
|
|
3101
|
+
const { client, tag, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tag);
|
|
2967
3102
|
if (versions.length === 0)
|
|
2968
3103
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
2969
3104
|
const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
|
|
2970
3105
|
if (targets.length === 0)
|
|
2971
3106
|
throw new Error(`Workflow definition ${definition} v${version} not deployed`);
|
|
2972
3107
|
const lastVersionGoes = targets.length === versions.length;
|
|
2973
|
-
await assertNoSpawnReferrers(client,
|
|
2974
|
-
const instanceIds = await nonTerminalInstanceIds(client,
|
|
3108
|
+
await assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes);
|
|
3109
|
+
const instanceIds = await nonTerminalInstanceIds(client, tag, definition, version);
|
|
2975
3110
|
if (instanceIds.length > 0 && cascade2 !== !0)
|
|
2976
3111
|
throw new Error(
|
|
2977
3112
|
`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.`
|
|
@@ -2992,7 +3127,7 @@ async function deleteDefinition(args) {
|
|
|
2992
3127
|
workflowResource: args.workflowResource,
|
|
2993
3128
|
definition,
|
|
2994
3129
|
definitions: versions,
|
|
2995
|
-
|
|
3130
|
+
tag
|
|
2996
3131
|
}) : 0;
|
|
2997
3132
|
return {
|
|
2998
3133
|
name: definition,
|
|
@@ -3001,10 +3136,10 @@ async function deleteDefinition(args) {
|
|
|
3001
3136
|
deletedGuardCount
|
|
3002
3137
|
};
|
|
3003
3138
|
}
|
|
3004
|
-
async function assertNoSpawnReferrers(client,
|
|
3139
|
+
async function assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes) {
|
|
3005
3140
|
const deployed = await client.fetch(
|
|
3006
3141
|
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
|
|
3007
|
-
{
|
|
3142
|
+
{ tag }
|
|
3008
3143
|
), 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(
|
|
3009
3144
|
(d) => refsOf(d).some(
|
|
3010
3145
|
(ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
|
|
@@ -3017,11 +3152,11 @@ async function assertNoSpawnReferrers(client, tags, definition, targets, lastVer
|
|
|
3017
3152
|
);
|
|
3018
3153
|
}
|
|
3019
3154
|
}
|
|
3020
|
-
async function nonTerminalInstanceIds(client,
|
|
3155
|
+
async function nonTerminalInstanceIds(client, tag, definition, version) {
|
|
3021
3156
|
const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
|
|
3022
3157
|
return (await client.fetch(
|
|
3023
3158
|
`*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
|
|
3024
|
-
{ definition,
|
|
3159
|
+
{ definition, tag, ...version !== void 0 ? { version } : {} }
|
|
3025
3160
|
)).map((doc) => doc._id);
|
|
3026
3161
|
}
|
|
3027
3162
|
async function abortInstances(args) {
|
|
@@ -3052,20 +3187,20 @@ const workflow = {
|
|
|
3052
3187
|
* Cycles in the dependency graph error before any write happens.
|
|
3053
3188
|
*/
|
|
3054
3189
|
deployDefinitions: async (args) => {
|
|
3055
|
-
const { client,
|
|
3056
|
-
|
|
3190
|
+
const { client, tag, workflowResource, definitions } = args;
|
|
3191
|
+
validateTag(tag);
|
|
3057
3192
|
for (const def of definitions)
|
|
3058
3193
|
validateDefinition(def);
|
|
3059
|
-
const ordered = await sortByDependencies(client, definitions,
|
|
3194
|
+
const ordered = await sortByDependencies(client, definitions, tag, workflowResource), results = [], tx = client.transaction();
|
|
3060
3195
|
let hasWrites = !1;
|
|
3061
3196
|
for (const def of ordered) {
|
|
3062
|
-
const id = definitionDocId(workflowResource,
|
|
3197
|
+
const id = definitionDocId(workflowResource, tag, def.name, def.version), expected = {
|
|
3063
3198
|
...def,
|
|
3064
3199
|
_id: id,
|
|
3065
3200
|
_type: WORKFLOW_DEFINITION_TYPE,
|
|
3066
|
-
|
|
3201
|
+
tag
|
|
3067
3202
|
}, existing = await client.getDocument(id);
|
|
3068
|
-
if (existing &&
|
|
3203
|
+
if (existing && existing.tag === tag && isDefinitionUnchanged(existing, expected)) {
|
|
3069
3204
|
results.push({ definition: def.name, version: def.version, status: "unchanged" });
|
|
3070
3205
|
continue;
|
|
3071
3206
|
}
|
|
@@ -3103,7 +3238,7 @@ const workflow = {
|
|
|
3103
3238
|
startInstance: async (args) => {
|
|
3104
3239
|
const {
|
|
3105
3240
|
client,
|
|
3106
|
-
|
|
3241
|
+
tag,
|
|
3107
3242
|
workflowResource,
|
|
3108
3243
|
definition: definitionName,
|
|
3109
3244
|
version,
|
|
@@ -3112,7 +3247,7 @@ const workflow = {
|
|
|
3112
3247
|
effectsContext,
|
|
3113
3248
|
instanceId,
|
|
3114
3249
|
perspective
|
|
3115
|
-
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version,
|
|
3250
|
+
} = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tag), id = instanceId ?? `${tag}.wf-instance.${randomKey()}`;
|
|
3116
3251
|
if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
|
|
3117
3252
|
throw new Error(
|
|
3118
3253
|
`Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
|
|
@@ -3124,7 +3259,7 @@ const workflow = {
|
|
|
3124
3259
|
client,
|
|
3125
3260
|
now,
|
|
3126
3261
|
selfId: id,
|
|
3127
|
-
|
|
3262
|
+
tag,
|
|
3128
3263
|
workflowResource,
|
|
3129
3264
|
...perspective !== void 0 ? { perspective } : {}
|
|
3130
3265
|
},
|
|
@@ -3132,7 +3267,7 @@ const workflow = {
|
|
|
3132
3267
|
}), effectivePerspective = perspective ?? derivePerspectiveFromState(resolvedState), base = buildInstanceBase({
|
|
3133
3268
|
id,
|
|
3134
3269
|
now,
|
|
3135
|
-
|
|
3270
|
+
tag,
|
|
3136
3271
|
workflowResource,
|
|
3137
3272
|
definitionName: definition.name,
|
|
3138
3273
|
pinnedVersion: definition.version,
|
|
@@ -3144,7 +3279,7 @@ const workflow = {
|
|
|
3144
3279
|
initialStage: definition.initialStage,
|
|
3145
3280
|
actor
|
|
3146
3281
|
});
|
|
3147
|
-
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id,
|
|
3282
|
+
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);
|
|
3148
3283
|
},
|
|
3149
3284
|
/**
|
|
3150
3285
|
* Fire an action against a task. If the task is pending, it is
|
|
@@ -3159,19 +3294,19 @@ const workflow = {
|
|
|
3159
3294
|
fireAction: async (args) => {
|
|
3160
3295
|
const {
|
|
3161
3296
|
client,
|
|
3162
|
-
|
|
3297
|
+
tag,
|
|
3163
3298
|
instanceId,
|
|
3164
3299
|
task,
|
|
3165
3300
|
action,
|
|
3166
3301
|
params,
|
|
3167
3302
|
idempotent,
|
|
3168
3303
|
resourceClients
|
|
3169
|
-
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId,
|
|
3304
|
+
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3170
3305
|
if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
|
|
3171
3306
|
return { instance: before, cascaded: 0, fired: !1 };
|
|
3172
3307
|
const evaluation = await evaluateInstance({
|
|
3173
3308
|
client,
|
|
3174
|
-
|
|
3309
|
+
tag,
|
|
3175
3310
|
instanceId,
|
|
3176
3311
|
access,
|
|
3177
3312
|
clock,
|
|
@@ -3190,7 +3325,7 @@ const workflow = {
|
|
|
3190
3325
|
...params !== void 0 ? { params } : {}
|
|
3191
3326
|
}), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3192
3327
|
return {
|
|
3193
|
-
instance: await reload(client, instanceId,
|
|
3328
|
+
instance: await reload(client, instanceId, tag),
|
|
3194
3329
|
cascaded,
|
|
3195
3330
|
fired: !0,
|
|
3196
3331
|
...ranOps !== void 0 ? { ranOps } : {}
|
|
@@ -3204,7 +3339,7 @@ const workflow = {
|
|
|
3204
3339
|
* reference them. Cascades after.
|
|
3205
3340
|
*/
|
|
3206
3341
|
completeEffect: async (args) => {
|
|
3207
|
-
const { client,
|
|
3342
|
+
const { client, tag, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3208
3343
|
await completeEffect({
|
|
3209
3344
|
client,
|
|
3210
3345
|
instanceId,
|
|
@@ -3218,7 +3353,7 @@ const workflow = {
|
|
|
3218
3353
|
});
|
|
3219
3354
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3220
3355
|
return {
|
|
3221
|
-
instance: await reload(client, instanceId,
|
|
3356
|
+
instance: await reload(client, instanceId, tag),
|
|
3222
3357
|
cascaded,
|
|
3223
3358
|
fired: !0
|
|
3224
3359
|
};
|
|
@@ -3233,11 +3368,11 @@ const workflow = {
|
|
|
3233
3368
|
* affected instances and the engine re-evaluates.
|
|
3234
3369
|
*/
|
|
3235
3370
|
tick: async (args) => {
|
|
3236
|
-
const { client,
|
|
3371
|
+
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);
|
|
3237
3372
|
await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
|
|
3238
3373
|
const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
|
|
3239
3374
|
return {
|
|
3240
|
-
instance: await reload(client, instanceId,
|
|
3375
|
+
instance: await reload(client, instanceId, tag),
|
|
3241
3376
|
cascaded,
|
|
3242
3377
|
fired: cascaded > 0
|
|
3243
3378
|
};
|
|
@@ -3248,8 +3383,8 @@ const workflow = {
|
|
|
3248
3383
|
* upstream; this verb performs the mechanical move.
|
|
3249
3384
|
*/
|
|
3250
3385
|
setStage: async (args) => {
|
|
3251
|
-
const { client,
|
|
3252
|
-
await reload(client, instanceId,
|
|
3386
|
+
const { client, tag, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3387
|
+
await reload(client, instanceId, tag);
|
|
3253
3388
|
const result = await setStage({
|
|
3254
3389
|
client,
|
|
3255
3390
|
instanceId,
|
|
@@ -3258,7 +3393,7 @@ const workflow = {
|
|
|
3258
3393
|
options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
|
|
3259
3394
|
}), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
|
|
3260
3395
|
return {
|
|
3261
|
-
instance: await reload(client, instanceId,
|
|
3396
|
+
instance: await reload(client, instanceId, tag),
|
|
3262
3397
|
cascaded,
|
|
3263
3398
|
fired: result.fired
|
|
3264
3399
|
};
|
|
@@ -3270,28 +3405,28 @@ const workflow = {
|
|
|
3270
3405
|
* contract (propagated, not cascaded — the instance is terminal).
|
|
3271
3406
|
*/
|
|
3272
3407
|
abortInstance: async (args) => {
|
|
3273
|
-
const { client,
|
|
3274
|
-
await reload(client, instanceId,
|
|
3408
|
+
const { client, tag, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
|
|
3409
|
+
await reload(client, instanceId, tag);
|
|
3275
3410
|
const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
|
|
3276
3411
|
return {
|
|
3277
|
-
instance: await reload(client, instanceId,
|
|
3412
|
+
instance: await reload(client, instanceId, tag),
|
|
3278
3413
|
cascaded: 0,
|
|
3279
3414
|
fired
|
|
3280
3415
|
};
|
|
3281
3416
|
},
|
|
3282
3417
|
/**
|
|
3283
|
-
* Fetch a workflow instance by id, scoped to the engine's
|
|
3418
|
+
* Fetch a workflow instance by id, scoped to the engine's tag.
|
|
3284
3419
|
* Throws when the instance doesn't exist or isn't visible to this
|
|
3285
3420
|
* engine.
|
|
3286
3421
|
*/
|
|
3287
3422
|
getInstance: async (args) => {
|
|
3288
|
-
const { client,
|
|
3289
|
-
return
|
|
3423
|
+
const { client, tag, instanceId } = args;
|
|
3424
|
+
return validateTag(tag), reload(client, instanceId, tag);
|
|
3290
3425
|
},
|
|
3291
3426
|
guardsForInstance: async (args) => {
|
|
3292
|
-
const { client,
|
|
3293
|
-
|
|
3294
|
-
const instance = await reload(client, instanceId,
|
|
3427
|
+
const { client, tag, instanceId, resourceClients } = args;
|
|
3428
|
+
validateTag(tag);
|
|
3429
|
+
const instance = await reload(client, instanceId, tag);
|
|
3295
3430
|
return guardsForInstance({
|
|
3296
3431
|
client,
|
|
3297
3432
|
clientForGdr: buildClientForGdr(client, resourceClients),
|
|
@@ -3299,9 +3434,9 @@ const workflow = {
|
|
|
3299
3434
|
});
|
|
3300
3435
|
},
|
|
3301
3436
|
guardsForDefinition: async (args) => {
|
|
3302
|
-
const { client,
|
|
3303
|
-
|
|
3304
|
-
const definitions = await loadDefinitionVersions(client, definition,
|
|
3437
|
+
const { client, tag, workflowResource, definition, resourceClients } = args;
|
|
3438
|
+
validateTag(tag);
|
|
3439
|
+
const definitions = await loadDefinitionVersions(client, definition, tag);
|
|
3305
3440
|
if (definitions.length === 0)
|
|
3306
3441
|
throw new Error(`No deployed definition for workflow ${definition}`);
|
|
3307
3442
|
return guardsForDefinition({
|
|
@@ -3313,22 +3448,21 @@ const workflow = {
|
|
|
3313
3448
|
});
|
|
3314
3449
|
},
|
|
3315
3450
|
/**
|
|
3316
|
-
* Run a caller-supplied GROQ query with the engine's
|
|
3317
|
-
* `$
|
|
3451
|
+
* Run a caller-supplied GROQ query with the engine's tag bound as
|
|
3452
|
+
* `$tag`. This does NOT rewrite the query — arbitrary GROQ
|
|
3318
3453
|
* can't be safely tag-scoped after the fact — so the CALLER MUST
|
|
3319
|
-
* filter on `$
|
|
3320
|
-
*
|
|
3321
|
-
* cross-tenant reads, a query that never references `$engineTags` is
|
|
3454
|
+
* filter on `$tag` (e.g. `tag == $tag`). To guard against accidental
|
|
3455
|
+
* cross-partition reads, a query that never references `$tag` is
|
|
3322
3456
|
* rejected before it reaches the lake. Caller is responsible for type
|
|
3323
3457
|
* narrowing the result.
|
|
3324
3458
|
*/
|
|
3325
3459
|
query: async (args) => {
|
|
3326
|
-
const { client,
|
|
3327
|
-
if (
|
|
3460
|
+
const { client, tag, groq, params } = args;
|
|
3461
|
+
if (validateTag(tag), !/\$tag\b/.test(groq))
|
|
3328
3462
|
throw new Error(
|
|
3329
|
-
`workflow.query: query must filter on $
|
|
3463
|
+
`workflow.query: query must filter on $tag to stay tag-scoped (e.g. "tag == $tag"); got: ${groq}`
|
|
3330
3464
|
);
|
|
3331
|
-
return client.fetch(groq, { ...params,
|
|
3465
|
+
return client.fetch(groq, { ...params, tag });
|
|
3332
3466
|
},
|
|
3333
3467
|
/**
|
|
3334
3468
|
* Snapshot-aware GROQ — runs against the same in-memory view that
|
|
@@ -3347,9 +3481,9 @@ const workflow = {
|
|
|
3347
3481
|
* without re-implementing hydration. Pure read — never writes.
|
|
3348
3482
|
*/
|
|
3349
3483
|
queryInScope: async (args) => {
|
|
3350
|
-
const { client,
|
|
3351
|
-
|
|
3352
|
-
const instance = await reload(client, instanceId,
|
|
3484
|
+
const { client, tag, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
|
|
3485
|
+
validateTag(tag);
|
|
3486
|
+
const instance = await reload(client, instanceId, tag), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = parse(groq, { params: { ...reserved, ...params } });
|
|
3353
3487
|
return await (await evaluate(tree, {
|
|
3354
3488
|
dataset: snapshot.docs,
|
|
3355
3489
|
params: { ...reserved, ...params }
|
|
@@ -3359,13 +3493,13 @@ const workflow = {
|
|
|
3359
3493
|
* List every pending effect on the instance. Returns the same entries
|
|
3360
3494
|
* the runtime would see — claimed and unclaimed alike.
|
|
3361
3495
|
*/
|
|
3362
|
-
listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.
|
|
3496
|
+
listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tag)).pendingEffects,
|
|
3363
3497
|
/**
|
|
3364
3498
|
* Filter pending effects on the instance by criteria. `claimed`
|
|
3365
3499
|
* filters on claim presence; `names` restricts to specific effect
|
|
3366
3500
|
* names. Both filters compose (AND).
|
|
3367
3501
|
*/
|
|
3368
|
-
findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.
|
|
3502
|
+
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))),
|
|
3369
3503
|
/**
|
|
3370
3504
|
* Project the instance from a given actor's perspective. Returns a
|
|
3371
3505
|
* `WorkflowEvaluation` with per-action verdicts (`allowed` + a
|
|
@@ -3385,8 +3519,8 @@ const workflow = {
|
|
|
3385
3519
|
* Pure read.
|
|
3386
3520
|
*/
|
|
3387
3521
|
diagnose: async (args) => {
|
|
3388
|
-
const evaluation = await evaluateInstance(args);
|
|
3389
|
-
return { evaluation, diagnosis:
|
|
3522
|
+
const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
|
|
3523
|
+
return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
|
|
3390
3524
|
},
|
|
3391
3525
|
/**
|
|
3392
3526
|
* List the actions an actor could fire on an instance's current stage,
|
|
@@ -3406,18 +3540,18 @@ const workflow = {
|
|
|
3406
3540
|
* record. (The per-task `spawnedInstances` field on the active stage
|
|
3407
3541
|
* only exists while that stage is current; history survives.) Strips
|
|
3408
3542
|
* the GDR URI on each `instanceRef` to a bare `_id`, fetches the
|
|
3409
|
-
* instances, drops any that aren't visible to this engine's
|
|
3543
|
+
* instances, drops any that aren't visible to this engine's tag, and
|
|
3410
3544
|
* returns them sorted by `startedAt` ascending.
|
|
3411
3545
|
*
|
|
3412
3546
|
* Pass `task` to restrict to a single spawning task on the parent.
|
|
3413
3547
|
*/
|
|
3414
3548
|
children: async (args) => {
|
|
3415
|
-
const { client,
|
|
3416
|
-
|
|
3417
|
-
const parent = await reload(client, instanceId,
|
|
3549
|
+
const { client, tag, instanceId, task } = args;
|
|
3550
|
+
validateTag(tag);
|
|
3551
|
+
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));
|
|
3418
3552
|
return ids.length === 0 ? [] : client.fetch(
|
|
3419
3553
|
`*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
|
|
3420
|
-
{ ids,
|
|
3554
|
+
{ ids, tag }
|
|
3421
3555
|
);
|
|
3422
3556
|
},
|
|
3423
3557
|
/**
|
|
@@ -3432,11 +3566,11 @@ const workflow = {
|
|
|
3432
3566
|
}
|
|
3433
3567
|
};
|
|
3434
3568
|
async function verifyDeployedDefinitionsInternal(args) {
|
|
3435
|
-
const { client,
|
|
3436
|
-
|
|
3569
|
+
const { client, tag, effectHandlers, missingHandler, logger } = args;
|
|
3570
|
+
validateTag(tag);
|
|
3437
3571
|
const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
|
|
3438
3572
|
`*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
|
|
3439
|
-
{
|
|
3573
|
+
{ tag }
|
|
3440
3574
|
), seen = [], missingByName = /* @__PURE__ */ new Map();
|
|
3441
3575
|
for (const def of definitions)
|
|
3442
3576
|
seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
|
|
@@ -3485,7 +3619,7 @@ function walkEffectNames(def, visit) {
|
|
|
3485
3619
|
async function drainEffectsInternal(args) {
|
|
3486
3620
|
const {
|
|
3487
3621
|
client,
|
|
3488
|
-
|
|
3622
|
+
tag,
|
|
3489
3623
|
workflowResource,
|
|
3490
3624
|
resourceClients,
|
|
3491
3625
|
instanceId,
|
|
@@ -3496,10 +3630,10 @@ async function drainEffectsInternal(args) {
|
|
|
3496
3630
|
actor: drainerActor,
|
|
3497
3631
|
...args.access?.grants !== void 0 ? { grants: args.access.grants } : {}
|
|
3498
3632
|
};
|
|
3499
|
-
|
|
3633
|
+
validateTag(tag);
|
|
3500
3634
|
const log = logger("drainEffects"), drained = [], failed = [], skipped = [], skippedKeys = /* @__PURE__ */ new Set();
|
|
3501
3635
|
for (; ; ) {
|
|
3502
|
-
const before = await reload(client, instanceId,
|
|
3636
|
+
const before = await reload(client, instanceId, tag), candidate = before.pendingEffects.find(
|
|
3503
3637
|
(e) => e.claim === void 0 && !skippedKeys.has(e._key)
|
|
3504
3638
|
);
|
|
3505
3639
|
if (candidate === void 0) break;
|
|
@@ -3516,7 +3650,7 @@ async function drainEffectsInternal(args) {
|
|
|
3516
3650
|
});
|
|
3517
3651
|
await reportEffectOutcome({
|
|
3518
3652
|
client,
|
|
3519
|
-
|
|
3653
|
+
tag,
|
|
3520
3654
|
workflowResource,
|
|
3521
3655
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3522
3656
|
instanceId,
|
|
@@ -3555,7 +3689,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
|
|
|
3555
3689
|
claimedBy: drainerActor
|
|
3556
3690
|
};
|
|
3557
3691
|
try {
|
|
3558
|
-
return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
|
|
3692
|
+
return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
|
|
3559
3693
|
} catch (error) {
|
|
3560
3694
|
if (!isRevisionConflict(error)) throw error;
|
|
3561
3695
|
return !1;
|
|
@@ -3586,7 +3720,7 @@ async function applyMissingHandler(policy, info, log) {
|
|
|
3586
3720
|
}
|
|
3587
3721
|
}
|
|
3588
3722
|
function createInstanceSession(args) {
|
|
3589
|
-
const { client,
|
|
3723
|
+
const { client, tag, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
|
|
3590
3724
|
let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
|
|
3591
3725
|
const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
|
|
3592
3726
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -3647,7 +3781,7 @@ function createInstanceSession(args) {
|
|
|
3647
3781
|
return commit(async (actor, held) => {
|
|
3648
3782
|
await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
|
|
3649
3783
|
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3650
|
-
return instance = await reload(client, instance._id,
|
|
3784
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: cascaded > 0 };
|
|
3651
3785
|
});
|
|
3652
3786
|
},
|
|
3653
3787
|
fireAction({ task, action, params }) {
|
|
@@ -3664,7 +3798,7 @@ function createInstanceSession(args) {
|
|
|
3664
3798
|
...clock !== void 0 ? { clock } : {},
|
|
3665
3799
|
...params !== void 0 ? { params } : {}
|
|
3666
3800
|
}), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
3667
|
-
return instance = await reload(client, instance._id,
|
|
3801
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3668
3802
|
});
|
|
3669
3803
|
}
|
|
3670
3804
|
};
|
|
@@ -3692,11 +3826,11 @@ const defaultLoggerFactory = (name) => ({
|
|
|
3692
3826
|
}
|
|
3693
3827
|
};
|
|
3694
3828
|
function createEngine(args) {
|
|
3695
|
-
const { client, workflowResource,
|
|
3696
|
-
|
|
3829
|
+
const { client, workflowResource, resourceClients, clock, tag } = args;
|
|
3830
|
+
validateTag(tag);
|
|
3697
3831
|
const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
|
|
3698
3832
|
client,
|
|
3699
|
-
|
|
3833
|
+
tag,
|
|
3700
3834
|
workflowResource,
|
|
3701
3835
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3702
3836
|
...clock !== void 0 ? { clock } : {},
|
|
@@ -3704,7 +3838,7 @@ function createEngine(args) {
|
|
|
3704
3838
|
});
|
|
3705
3839
|
return {
|
|
3706
3840
|
client,
|
|
3707
|
-
|
|
3841
|
+
tag,
|
|
3708
3842
|
workflowResource,
|
|
3709
3843
|
effectHandlers,
|
|
3710
3844
|
missingHandler,
|
|
@@ -3720,11 +3854,11 @@ function createEngine(args) {
|
|
|
3720
3854
|
setStage: (rest) => workflow.setStage(bind(rest)),
|
|
3721
3855
|
abortInstance: (rest) => workflow.abortInstance(bind(rest)),
|
|
3722
3856
|
deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
|
|
3723
|
-
getInstance: ({ instanceId }) => workflow.getInstance({ client,
|
|
3724
|
-
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client,
|
|
3857
|
+
getInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }),
|
|
3858
|
+
subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
|
|
3725
3859
|
instance: (instanceDoc, opts) => createInstanceSession({
|
|
3726
3860
|
client,
|
|
3727
|
-
|
|
3861
|
+
tag,
|
|
3728
3862
|
instance: instanceDoc,
|
|
3729
3863
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3730
3864
|
...clock !== void 0 ? { clock } : {},
|
|
@@ -3733,45 +3867,45 @@ function createEngine(args) {
|
|
|
3733
3867
|
}),
|
|
3734
3868
|
guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
|
|
3735
3869
|
client,
|
|
3736
|
-
|
|
3870
|
+
tag,
|
|
3737
3871
|
workflowResource,
|
|
3738
3872
|
instanceId,
|
|
3739
3873
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3740
3874
|
}),
|
|
3741
3875
|
guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
|
|
3742
3876
|
client,
|
|
3743
|
-
|
|
3877
|
+
tag,
|
|
3744
3878
|
workflowResource,
|
|
3745
3879
|
definition,
|
|
3746
3880
|
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3747
3881
|
}),
|
|
3748
3882
|
children: ({ instanceId, task }) => workflow.children({
|
|
3749
3883
|
client,
|
|
3750
|
-
|
|
3884
|
+
tag,
|
|
3751
3885
|
workflowResource,
|
|
3752
3886
|
instanceId,
|
|
3753
3887
|
...task !== void 0 ? { task } : {}
|
|
3754
3888
|
}),
|
|
3755
3889
|
query: ({ groq, params }) => workflow.query({
|
|
3756
3890
|
client,
|
|
3757
|
-
|
|
3891
|
+
tag,
|
|
3758
3892
|
workflowResource,
|
|
3759
3893
|
groq,
|
|
3760
3894
|
...params !== void 0 ? { params } : {}
|
|
3761
3895
|
}),
|
|
3762
3896
|
queryInScope: ({ instanceId, groq, params }) => workflow.queryInScope({
|
|
3763
3897
|
client,
|
|
3764
|
-
|
|
3898
|
+
tag,
|
|
3765
3899
|
workflowResource,
|
|
3766
3900
|
instanceId,
|
|
3767
3901
|
groq,
|
|
3768
3902
|
...params !== void 0 ? { params } : {},
|
|
3769
3903
|
...clock !== void 0 ? { clock } : {}
|
|
3770
3904
|
}),
|
|
3771
|
-
listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client,
|
|
3905
|
+
listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tag, workflowResource, instanceId }),
|
|
3772
3906
|
findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
|
|
3773
3907
|
client,
|
|
3774
|
-
|
|
3908
|
+
tag,
|
|
3775
3909
|
workflowResource,
|
|
3776
3910
|
instanceId,
|
|
3777
3911
|
...claimed !== void 0 ? { claimed } : {},
|
|
@@ -3779,7 +3913,7 @@ function createEngine(args) {
|
|
|
3779
3913
|
}),
|
|
3780
3914
|
drainEffects: ({ instanceId, access }) => drainEffectsInternal({
|
|
3781
3915
|
client,
|
|
3782
|
-
|
|
3916
|
+
tag,
|
|
3783
3917
|
workflowResource,
|
|
3784
3918
|
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3785
3919
|
instanceId,
|
|
@@ -3790,7 +3924,7 @@ function createEngine(args) {
|
|
|
3790
3924
|
}),
|
|
3791
3925
|
verifyDeployedDefinitions: () => verifyDeployedDefinitionsInternal({
|
|
3792
3926
|
client,
|
|
3793
|
-
|
|
3927
|
+
tag,
|
|
3794
3928
|
effectHandlers,
|
|
3795
3929
|
missingHandler,
|
|
3796
3930
|
logger
|
|
@@ -3798,7 +3932,7 @@ function createEngine(args) {
|
|
|
3798
3932
|
};
|
|
3799
3933
|
}
|
|
3800
3934
|
function docIdFor(def, target) {
|
|
3801
|
-
return definitionDocId(target.workflowResource,
|
|
3935
|
+
return definitionDocId(target.workflowResource, target.tag, def.name, def.version);
|
|
3802
3936
|
}
|
|
3803
3937
|
function diffStatus(existing, expected) {
|
|
3804
3938
|
return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
|
|
@@ -3808,7 +3942,7 @@ function diffEntry(def, existingRaw, target) {
|
|
|
3808
3942
|
...def,
|
|
3809
3943
|
_id: docId,
|
|
3810
3944
|
_type: WORKFLOW_DEFINITION_TYPE,
|
|
3811
|
-
|
|
3945
|
+
tag: target.tag
|
|
3812
3946
|
}, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
|
|
3813
3947
|
return {
|
|
3814
3948
|
name: def.name,
|
|
@@ -4008,6 +4142,7 @@ export {
|
|
|
4008
4142
|
ActionParamsInvalidError,
|
|
4009
4143
|
CascadeLimitError,
|
|
4010
4144
|
ConcurrentFireActionError,
|
|
4145
|
+
DEFAULT_CONTENT_PERSPECTIVE,
|
|
4011
4146
|
DISPLAY,
|
|
4012
4147
|
EFFECTS_CONTEXT_DISPLAY,
|
|
4013
4148
|
GUARD_DOC_TYPE,
|
|
@@ -4022,10 +4157,8 @@ export {
|
|
|
4022
4157
|
WorkflowStateDivergedError,
|
|
4023
4158
|
abortReason,
|
|
4024
4159
|
actionVerdict,
|
|
4025
|
-
assigneesOf,
|
|
4026
4160
|
availableActions,
|
|
4027
4161
|
buildSnapshot,
|
|
4028
|
-
canonicalTag,
|
|
4029
4162
|
compileGuard,
|
|
4030
4163
|
computeDiffEntries,
|
|
4031
4164
|
contentReleaseName,
|
|
@@ -4055,13 +4188,13 @@ export {
|
|
|
4055
4188
|
isGdr,
|
|
4056
4189
|
isTerminalStage,
|
|
4057
4190
|
lakeGuardId,
|
|
4058
|
-
openStage,
|
|
4059
4191
|
parseGdr,
|
|
4060
4192
|
readsRaw,
|
|
4061
4193
|
refCanvas,
|
|
4062
4194
|
refDashboard,
|
|
4063
4195
|
refDataset,
|
|
4064
4196
|
refMediaLibrary,
|
|
4197
|
+
remediationsFor,
|
|
4065
4198
|
resolveAccess,
|
|
4066
4199
|
resourceFromParsed,
|
|
4067
4200
|
retractStageGuards,
|
|
@@ -4071,7 +4204,7 @@ export {
|
|
|
4071
4204
|
subscriptionDocumentsForInstance,
|
|
4072
4205
|
tagScopeFilter,
|
|
4073
4206
|
validateDefinition,
|
|
4074
|
-
|
|
4207
|
+
validateTag,
|
|
4075
4208
|
verdictGuardsForInstance,
|
|
4076
4209
|
wallClock,
|
|
4077
4210
|
workflow
|