@sanity/workflow-engine 0.6.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 +216 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +323 -19
- package/dist/index.d.ts +323 -19
- package/dist/index.js +216 -67
- 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,7 +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 GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
815
|
+
const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
|
|
726
816
|
function resolveGuard(guard, instance, stageName, now) {
|
|
727
817
|
const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
|
|
728
818
|
if (targets === null) return null;
|
|
@@ -749,11 +839,11 @@ function resolveGuard(guard, instance, stageName, now) {
|
|
|
749
839
|
}
|
|
750
840
|
async function upsertGuard(client, doc) {
|
|
751
841
|
if (!await client.getDocument(doc._id)) {
|
|
752
|
-
await client.create(doc);
|
|
842
|
+
await client.create(doc, SYNC_COMMIT);
|
|
753
843
|
return;
|
|
754
844
|
}
|
|
755
845
|
const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
|
|
756
|
-
await client.patch(doc._id).set(body).commit();
|
|
846
|
+
await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
|
|
757
847
|
}
|
|
758
848
|
function resolvedStageGuards(args) {
|
|
759
849
|
const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
|
|
@@ -776,7 +866,7 @@ async function retractStageGuards(args) {
|
|
|
776
866
|
const live = await committedInstance(args);
|
|
777
867
|
if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
|
|
778
868
|
for (const { client, doc } of resolvedStageGuards(args))
|
|
779
|
-
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);
|
|
780
870
|
}
|
|
781
871
|
async function deleteOrphanedDefinitionGuards(args) {
|
|
782
872
|
const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
|
|
@@ -800,16 +890,32 @@ function randomKey(length = 12) {
|
|
|
800
890
|
const bytes = new Uint8Array(length);
|
|
801
891
|
return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
|
|
802
892
|
}
|
|
803
|
-
|
|
893
|
+
function isUnevaluable(result) {
|
|
894
|
+
return result == null;
|
|
895
|
+
}
|
|
896
|
+
async function evaluateConditionOutcome(args) {
|
|
804
897
|
const { condition, snapshot, params } = args;
|
|
805
|
-
|
|
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";
|
|
806
904
|
}
|
|
807
905
|
async function evaluatePredicates(args) {
|
|
808
906
|
const out = {};
|
|
809
|
-
for (const [name, groq] of Object.entries(args.predicates ?? {}))
|
|
810
|
-
|
|
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
|
+
}
|
|
811
911
|
return out;
|
|
812
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
|
+
}
|
|
813
919
|
async function runGroq(groq, params, snapshot) {
|
|
814
920
|
const tree = parse(groq, { params });
|
|
815
921
|
return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
|
|
@@ -977,7 +1083,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
|
|
|
977
1083
|
tag: ctx.tag
|
|
978
1084
|
});
|
|
979
1085
|
try {
|
|
980
|
-
const fetchOptions =
|
|
1086
|
+
const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
|
|
981
1087
|
return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
|
|
982
1088
|
} catch (err) {
|
|
983
1089
|
throw new Error(
|
|
@@ -1107,13 +1213,16 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1107
1213
|
params
|
|
1108
1214
|
}), ...params };
|
|
1109
1215
|
}
|
|
1110
|
-
async function
|
|
1111
|
-
return condition === void 0 ?
|
|
1216
|
+
async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
|
|
1217
|
+
return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
|
|
1112
1218
|
condition,
|
|
1113
1219
|
snapshot: ctx.snapshot,
|
|
1114
1220
|
params: await ctxConditionParams(ctx, opts)
|
|
1115
1221
|
});
|
|
1116
1222
|
}
|
|
1223
|
+
async function ctxEvaluateCondition(ctx, condition, opts) {
|
|
1224
|
+
return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
|
|
1225
|
+
}
|
|
1117
1226
|
async function buildEngineContext(args) {
|
|
1118
1227
|
const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
|
|
1119
1228
|
return {
|
|
@@ -1233,7 +1342,7 @@ async function deployOrRollback(args) {
|
|
|
1233
1342
|
});
|
|
1234
1343
|
try {
|
|
1235
1344
|
let patch = args.client.patch(args.instanceId).set(args.restore);
|
|
1236
|
-
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);
|
|
1237
1346
|
} catch (rollbackError) {
|
|
1238
1347
|
throw new WorkflowStateDivergedError({
|
|
1239
1348
|
instanceId: args.instanceId,
|
|
@@ -1340,7 +1449,7 @@ async function persist(ctx, mutation) {
|
|
|
1340
1449
|
lastChangedAt: ctx.now
|
|
1341
1450
|
}, pendingCreates = mutation.pendingCreates;
|
|
1342
1451
|
if (pendingCreates.length === 0)
|
|
1343
|
-
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);
|
|
1344
1453
|
const tx = ctx.client.transaction();
|
|
1345
1454
|
for (const body of pendingCreates) tx.create(body);
|
|
1346
1455
|
tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
|
|
@@ -1493,9 +1602,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1493
1602
|
const parentScope = await ctxConditionParams(ctx, {
|
|
1494
1603
|
taskName: task.name,
|
|
1495
1604
|
...actor ? { actor } : {}
|
|
1496
|
-
}), 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 = [];
|
|
1497
1606
|
for (const row of rows) {
|
|
1498
|
-
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({
|
|
1499
1615
|
client: ctx.client,
|
|
1500
1616
|
parent: ctx.instance,
|
|
1501
1617
|
definition,
|
|
@@ -1510,28 +1626,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
|
|
|
1510
1626
|
}
|
|
1511
1627
|
async function resolveSpawnRows(ctx, sub) {
|
|
1512
1628
|
const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
|
|
1513
|
-
let value;
|
|
1629
|
+
let value, discoveryResource;
|
|
1514
1630
|
if (groq.includes("*")) {
|
|
1515
|
-
const routingUri =
|
|
1516
|
-
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({
|
|
1517
1633
|
client,
|
|
1518
1634
|
groq,
|
|
1519
1635
|
params,
|
|
1520
|
-
|
|
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
|
|
1521
1640
|
});
|
|
1522
1641
|
} else {
|
|
1523
1642
|
const tree = parse(groq, { params });
|
|
1524
1643
|
value = await (await evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
|
|
1525
1644
|
}
|
|
1526
|
-
return Array.isArray(value) ? value : value == null ? [] : [value];
|
|
1527
|
-
}
|
|
1528
|
-
function firstDocRefGdrUri(entries) {
|
|
1529
|
-
if (entries) {
|
|
1530
|
-
for (const s of entries)
|
|
1531
|
-
if (s._type === "doc.ref" && s.value !== null) return s.value.id;
|
|
1532
|
-
}
|
|
1645
|
+
return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
|
|
1533
1646
|
}
|
|
1534
|
-
async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
1647
|
+
async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
|
|
1535
1648
|
const out = [];
|
|
1536
1649
|
for (const [name, groq] of Object.entries(sub.with ?? {})) {
|
|
1537
1650
|
const declared = (childDefinition.state ?? []).find((e) => e.name === name);
|
|
@@ -1539,7 +1652,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
|
|
|
1539
1652
|
throw new Error(
|
|
1540
1653
|
`subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
|
|
1541
1654
|
);
|
|
1542
|
-
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
|
+
});
|
|
1543
1661
|
value != null && out.push({
|
|
1544
1662
|
type: declared.type,
|
|
1545
1663
|
name,
|
|
@@ -1556,11 +1674,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
|
|
|
1556
1674
|
}
|
|
1557
1675
|
return out;
|
|
1558
1676
|
}
|
|
1559
|
-
function canonicaliseSpawnValue(kind, value,
|
|
1560
|
-
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
|
+
);
|
|
1561
1685
|
}
|
|
1562
|
-
function coerceSpawnGdr(raw,
|
|
1563
|
-
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));
|
|
1564
1688
|
if (raw == null) return null;
|
|
1565
1689
|
if (typeof raw == "object" && "id" in raw && "type" in raw) {
|
|
1566
1690
|
const r = raw;
|
|
@@ -2059,21 +2183,24 @@ function resolveMachineStep(mutation, task, entry, now) {
|
|
|
2059
2183
|
async function buildStageTasks(ctx, stage) {
|
|
2060
2184
|
const entries = [];
|
|
2061
2185
|
for (const task of stage.tasks ?? []) {
|
|
2062
|
-
const
|
|
2186
|
+
const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
|
|
2063
2187
|
entries.push({
|
|
2064
2188
|
_key: randomKey(),
|
|
2065
2189
|
name: task.name,
|
|
2066
2190
|
status: inScope ? "pending" : "skipped",
|
|
2067
|
-
...task.filter !== void 0 ? {
|
|
2068
|
-
filterEvaluation: {
|
|
2069
|
-
at: ctx.now,
|
|
2070
|
-
truthy: inScope
|
|
2071
|
-
}
|
|
2072
|
-
} : {}
|
|
2191
|
+
...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
|
|
2073
2192
|
});
|
|
2074
2193
|
}
|
|
2075
2194
|
return entries;
|
|
2076
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
|
+
}
|
|
2077
2204
|
function isTerminalStage(stage) {
|
|
2078
2205
|
return (stage.transitions ?? []).length === 0;
|
|
2079
2206
|
}
|
|
@@ -2223,8 +2350,11 @@ async function commitTransition(ctx, actor) {
|
|
|
2223
2350
|
};
|
|
2224
2351
|
}
|
|
2225
2352
|
async function pickTransition(ctx, stage) {
|
|
2226
|
-
for (const candidate of stage.transitions ?? [])
|
|
2227
|
-
|
|
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
|
+
}
|
|
2228
2358
|
}
|
|
2229
2359
|
async function evaluateAutoTransitions(args) {
|
|
2230
2360
|
const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
|
|
@@ -2254,7 +2384,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
|
|
|
2254
2384
|
}, committed = await client.patch(instance._id).set({
|
|
2255
2385
|
stages: [initialStageEntry],
|
|
2256
2386
|
lastChangedAt: now
|
|
2257
|
-
}).ifRevisionId(instance._rev).commit();
|
|
2387
|
+
}).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
|
|
2258
2388
|
await deployOrRollback({
|
|
2259
2389
|
client,
|
|
2260
2390
|
instanceId: instance._id,
|
|
@@ -2516,15 +2646,18 @@ async function evaluateFromSnapshot(args) {
|
|
|
2516
2646
|
})
|
|
2517
2647
|
);
|
|
2518
2648
|
const transitionEvaluations = [];
|
|
2519
|
-
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
|
+
});
|
|
2520
2655
|
transitionEvaluations.push({
|
|
2521
2656
|
transition,
|
|
2522
|
-
filterSatisfied:
|
|
2523
|
-
|
|
2524
|
-
snapshot,
|
|
2525
|
-
params: scope
|
|
2526
|
-
})
|
|
2657
|
+
filterSatisfied: outcome === "satisfied",
|
|
2658
|
+
unevaluable: outcome === "unevaluable"
|
|
2527
2659
|
});
|
|
2660
|
+
}
|
|
2528
2661
|
const currentStage = {
|
|
2529
2662
|
stage,
|
|
2530
2663
|
tasks: taskEvaluations,
|
|
@@ -2572,7 +2705,11 @@ async function evaluateTask(args) {
|
|
|
2572
2705
|
...scopedStateOverlay(instance, snapshot, task.name)
|
|
2573
2706
|
},
|
|
2574
2707
|
assigned
|
|
2575
|
-
},
|
|
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 = [];
|
|
2576
2713
|
for (const action of task.actions ?? [])
|
|
2577
2714
|
actions.push(
|
|
2578
2715
|
await evaluateAction({
|
|
@@ -2582,7 +2719,8 @@ async function evaluateTask(args) {
|
|
|
2582
2719
|
snapshot,
|
|
2583
2720
|
taskScope,
|
|
2584
2721
|
stageHasExits,
|
|
2585
|
-
guardDenial
|
|
2722
|
+
guardDenial,
|
|
2723
|
+
requirementsReason
|
|
2586
2724
|
})
|
|
2587
2725
|
);
|
|
2588
2726
|
return {
|
|
@@ -2593,12 +2731,22 @@ async function evaluateTask(args) {
|
|
|
2593
2731
|
// they're still inbox items. The inbox reads the assignees-kind state
|
|
2594
2732
|
// entry BY KIND (who-data is state).
|
|
2595
2733
|
pendingOnActor: (status === "active" || status === "pending") && assigned,
|
|
2734
|
+
...unmetRequirements.length > 0 ? { unmetRequirements } : {},
|
|
2596
2735
|
actions
|
|
2597
2736
|
};
|
|
2598
2737
|
}
|
|
2599
2738
|
async function evaluateAction(args) {
|
|
2600
|
-
const {
|
|
2601
|
-
|
|
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({
|
|
2602
2750
|
condition: action.filter,
|
|
2603
2751
|
snapshot,
|
|
2604
2752
|
params: taskScope
|
|
@@ -2875,7 +3023,8 @@ const disabledReasonDetail = {
|
|
|
2875
3023
|
"task-not-active": (r) => `task status is "${r.status}"`,
|
|
2876
3024
|
"stage-terminal": (r) => `stage "${r.stage}" is terminal`,
|
|
2877
3025
|
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
2878
|
-
"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(", ")}`
|
|
2879
3028
|
};
|
|
2880
3029
|
function formatDisabledReason(task, action, reason) {
|
|
2881
3030
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
@@ -3130,7 +3279,7 @@ const workflow = {
|
|
|
3130
3279
|
initialStage: definition.initialStage,
|
|
3131
3280
|
actor
|
|
3132
3281
|
});
|
|
3133
|
-
return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
|
|
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);
|
|
3134
3283
|
},
|
|
3135
3284
|
/**
|
|
3136
3285
|
* Fire an action against a task. If the task is pending, it is
|
|
@@ -3370,8 +3519,8 @@ const workflow = {
|
|
|
3370
3519
|
* Pure read.
|
|
3371
3520
|
*/
|
|
3372
3521
|
diagnose: async (args) => {
|
|
3373
|
-
const evaluation = await evaluateInstance(args);
|
|
3374
|
-
return { evaluation, diagnosis:
|
|
3522
|
+
const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
|
|
3523
|
+
return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
|
|
3375
3524
|
},
|
|
3376
3525
|
/**
|
|
3377
3526
|
* List the actions an actor could fire on an instance's current stage,
|
|
@@ -3540,7 +3689,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
|
|
|
3540
3689
|
claimedBy: drainerActor
|
|
3541
3690
|
};
|
|
3542
3691
|
try {
|
|
3543
|
-
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;
|
|
3544
3693
|
} catch (error) {
|
|
3545
3694
|
if (!isRevisionConflict(error)) throw error;
|
|
3546
3695
|
return !1;
|
|
@@ -3993,6 +4142,7 @@ export {
|
|
|
3993
4142
|
ActionParamsInvalidError,
|
|
3994
4143
|
CascadeLimitError,
|
|
3995
4144
|
ConcurrentFireActionError,
|
|
4145
|
+
DEFAULT_CONTENT_PERSPECTIVE,
|
|
3996
4146
|
DISPLAY,
|
|
3997
4147
|
EFFECTS_CONTEXT_DISPLAY,
|
|
3998
4148
|
GUARD_DOC_TYPE,
|
|
@@ -4007,7 +4157,6 @@ export {
|
|
|
4007
4157
|
WorkflowStateDivergedError,
|
|
4008
4158
|
abortReason,
|
|
4009
4159
|
actionVerdict,
|
|
4010
|
-
assigneesOf,
|
|
4011
4160
|
availableActions,
|
|
4012
4161
|
buildSnapshot,
|
|
4013
4162
|
compileGuard,
|
|
@@ -4039,13 +4188,13 @@ export {
|
|
|
4039
4188
|
isGdr,
|
|
4040
4189
|
isTerminalStage,
|
|
4041
4190
|
lakeGuardId,
|
|
4042
|
-
openStage,
|
|
4043
4191
|
parseGdr,
|
|
4044
4192
|
readsRaw,
|
|
4045
4193
|
refCanvas,
|
|
4046
4194
|
refDashboard,
|
|
4047
4195
|
refDataset,
|
|
4048
4196
|
refMediaLibrary,
|
|
4197
|
+
remediationsFor,
|
|
4049
4198
|
resolveAccess,
|
|
4050
4199
|
resourceFromParsed,
|
|
4051
4200
|
retractStageGuards,
|