@sanity/workflow-engine 0.8.0 → 0.9.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 +68 -21
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +68 -21
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +81 -18
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +535 -80
- package/dist/define.d.ts +535 -80
- package/dist/define.js +82 -19
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +430 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +968 -127
- package/dist/index.d.ts +968 -127
- package/dist/index.js +431 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -155,11 +155,11 @@ function scopedStateOverlay(instance, snapshot, taskName) {
|
|
|
155
155
|
const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
|
|
156
156
|
return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
|
|
157
157
|
}
|
|
158
|
-
function assignedFor(instance, taskName, actor) {
|
|
158
|
+
function assignedFor(instance, taskName, actor, roleAliases) {
|
|
159
159
|
if (actor === void 0) return !1;
|
|
160
160
|
const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
|
|
161
161
|
return entry === void 0 ? !1 : entry.value.some(
|
|
162
|
-
(a) => a.type === "user" ? a.id === actor.id : (actor.roles
|
|
162
|
+
(a) => a.type === "user" ? a.id === actor.id : schema.actorFulfillsRole(actor.roles, a.role, roleAliases)
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
function effectsContextMap(instance) {
|
|
@@ -678,6 +678,17 @@ function isRevisionConflict(error) {
|
|
|
678
678
|
const { statusCode, message } = error;
|
|
679
679
|
return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
|
|
680
680
|
}
|
|
681
|
+
class ConcurrentEditStateError extends Error {
|
|
682
|
+
instanceId;
|
|
683
|
+
target;
|
|
684
|
+
attempts;
|
|
685
|
+
constructor(args) {
|
|
686
|
+
const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
|
|
687
|
+
super(
|
|
688
|
+
`Edit of "${args.target.scope}:${where}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
|
|
689
|
+
), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
681
692
|
class CascadeLimitError extends Error {
|
|
682
693
|
instanceId;
|
|
683
694
|
limit;
|
|
@@ -1297,6 +1308,12 @@ function coerceToGdr(raw, workflowResource) {
|
|
|
1297
1308
|
function gdrToBareId(uri) {
|
|
1298
1309
|
return uri.includes(":") ? extractDocumentId(uri) : uri;
|
|
1299
1310
|
}
|
|
1311
|
+
function loadCallContext(client, instanceId, options) {
|
|
1312
|
+
return loadContext(client, instanceId, {
|
|
1313
|
+
...options?.clock ? { clock: options.clock } : {},
|
|
1314
|
+
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1300
1317
|
async function loadContext(client, instanceId, options) {
|
|
1301
1318
|
const instance = await client.getDocument(instanceId);
|
|
1302
1319
|
if (!instance)
|
|
@@ -1317,7 +1334,7 @@ async function ctxConditionParams(ctx, opts) {
|
|
|
1317
1334
|
...base,
|
|
1318
1335
|
state,
|
|
1319
1336
|
actor: opts?.actor,
|
|
1320
|
-
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
|
|
1337
|
+
assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
|
|
1321
1338
|
...opts?.vars
|
|
1322
1339
|
};
|
|
1323
1340
|
return { ...await evaluatePredicates({
|
|
@@ -2048,22 +2065,27 @@ function runOps(args) {
|
|
|
2048
2065
|
const summaries = [];
|
|
2049
2066
|
for (const op of ops) {
|
|
2050
2067
|
const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
|
|
2051
|
-
summaries.push(summary), mutation.history.push({
|
|
2052
|
-
_key: randomKey(),
|
|
2053
|
-
_type: "opApplied",
|
|
2054
|
-
at: now,
|
|
2055
|
-
stage,
|
|
2056
|
-
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2057
|
-
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2058
|
-
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2059
|
-
opType: summary.opType,
|
|
2060
|
-
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2061
|
-
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2062
|
-
...actor !== void 0 ? { actor } : {}
|
|
2063
|
-
});
|
|
2068
|
+
summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
|
|
2064
2069
|
}
|
|
2065
2070
|
return summaries;
|
|
2066
2071
|
}
|
|
2072
|
+
function opAppliedEntry(args) {
|
|
2073
|
+
const { origin, summary, stage, actor, now } = args;
|
|
2074
|
+
return {
|
|
2075
|
+
_key: randomKey(),
|
|
2076
|
+
_type: "opApplied",
|
|
2077
|
+
at: now,
|
|
2078
|
+
stage,
|
|
2079
|
+
...origin.task !== void 0 ? { task: origin.task } : {},
|
|
2080
|
+
...origin.action !== void 0 ? { action: origin.action } : {},
|
|
2081
|
+
...origin.transition !== void 0 ? { transition: origin.transition } : {},
|
|
2082
|
+
...origin.edit === !0 ? { edit: !0 } : {},
|
|
2083
|
+
opType: summary.opType,
|
|
2084
|
+
...summary.target !== void 0 ? { target: summary.target } : {},
|
|
2085
|
+
...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
|
|
2086
|
+
...actor !== void 0 ? { actor } : {}
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2067
2089
|
function applyOp(op, ctx) {
|
|
2068
2090
|
switch (op.type) {
|
|
2069
2091
|
case "state.set":
|
|
@@ -2697,6 +2719,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
|
|
|
2697
2719
|
return;
|
|
2698
2720
|
}
|
|
2699
2721
|
}
|
|
2722
|
+
function effectiveEditable(baseline, override) {
|
|
2723
|
+
if (baseline === void 0) return;
|
|
2724
|
+
if (override === void 0) return baseline;
|
|
2725
|
+
if (baseline === !0 && override === !0) return !0;
|
|
2726
|
+
const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
|
|
2727
|
+
return schema.andConditions(parts) ?? !0;
|
|
2728
|
+
}
|
|
2729
|
+
function slotSites(definition, stage) {
|
|
2730
|
+
const sites = [];
|
|
2731
|
+
for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
|
|
2732
|
+
for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
|
|
2733
|
+
for (const task of stage.tasks ?? [])
|
|
2734
|
+
for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
|
|
2735
|
+
return sites;
|
|
2736
|
+
}
|
|
2737
|
+
function toResolvedSlot(site, stage) {
|
|
2738
|
+
return {
|
|
2739
|
+
scope: site.scope,
|
|
2740
|
+
...site.task !== void 0 ? { task: site.task } : {},
|
|
2741
|
+
name: site.entry.name,
|
|
2742
|
+
type: site.entry.type,
|
|
2743
|
+
...site.entry.title !== void 0 ? { title: site.entry.title } : {},
|
|
2744
|
+
ref: { scope: site.scope, state: site.entry.name },
|
|
2745
|
+
effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
function editableSlotsInStage(definition, stage) {
|
|
2749
|
+
return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
|
|
2750
|
+
}
|
|
2751
|
+
function editTargetMatches(slot, target) {
|
|
2752
|
+
return slot.name !== target.state ? !1 : target.task !== void 0 ? slot.scope === "task" && slot.task === target.task : target.scope !== void 0 ? slot.scope === target.scope : !0;
|
|
2753
|
+
}
|
|
2754
|
+
const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
|
|
2755
|
+
function resolveEditTarget(args) {
|
|
2756
|
+
const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
|
|
2757
|
+
(site) => editTargetMatches(
|
|
2758
|
+
{
|
|
2759
|
+
scope: site.scope,
|
|
2760
|
+
name: site.entry.name,
|
|
2761
|
+
...site.task !== void 0 ? { task: site.task } : {}
|
|
2762
|
+
},
|
|
2763
|
+
target
|
|
2764
|
+
)
|
|
2765
|
+
).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
2766
|
+
return found ? toResolvedSlot(found, stage) : void 0;
|
|
2767
|
+
}
|
|
2768
|
+
function slotWindowOpen(instance, slot) {
|
|
2769
|
+
if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
|
|
2770
|
+
if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
|
|
2771
|
+
if (slot.scope !== "task") return { open: !0 };
|
|
2772
|
+
const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
|
|
2773
|
+
return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
|
|
2774
|
+
}
|
|
2775
|
+
function readSlotValue(instance, slot) {
|
|
2776
|
+
return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
|
|
2777
|
+
}
|
|
2778
|
+
function slotStateHost(instance, slot) {
|
|
2779
|
+
if (slot.scope === "workflow") return instance.state;
|
|
2780
|
+
const stageEntry = findOpenStageEntry(instance);
|
|
2781
|
+
return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
|
|
2782
|
+
}
|
|
2783
|
+
function slotProvenance(instance, ref) {
|
|
2784
|
+
let latest;
|
|
2785
|
+
for (const entry of instance.history)
|
|
2786
|
+
entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
|
|
2787
|
+
return latest === void 0 ? {} : {
|
|
2788
|
+
...latest.actor !== void 0 ? { setBy: latest.actor } : {},
|
|
2789
|
+
setAt: latest.at
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function editDisabledReason(args) {
|
|
2793
|
+
const { effective, instance, window, guardDenial, predicateSatisfied } = args;
|
|
2794
|
+
if (effective === void 0) return { kind: "not-editable" };
|
|
2795
|
+
if (!window.open)
|
|
2796
|
+
return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
|
|
2797
|
+
if (guardDenial !== void 0) return guardDenial;
|
|
2798
|
+
if (!predicateSatisfied)
|
|
2799
|
+
return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
|
|
2800
|
+
}
|
|
2700
2801
|
async function evaluateInstance(args) {
|
|
2701
2802
|
const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
|
|
2702
2803
|
validateTag(tag);
|
|
@@ -2730,6 +2831,7 @@ async function evaluateFromSnapshot(args) {
|
|
|
2730
2831
|
actor,
|
|
2731
2832
|
snapshot,
|
|
2732
2833
|
scope,
|
|
2834
|
+
roleAliases: definition.roleAliases,
|
|
2733
2835
|
stageHasExits: !isTerminalStage(stage),
|
|
2734
2836
|
guardDenial
|
|
2735
2837
|
})
|
|
@@ -2751,16 +2853,65 @@ async function evaluateFromSnapshot(args) {
|
|
|
2751
2853
|
stage,
|
|
2752
2854
|
tasks: taskEvaluations,
|
|
2753
2855
|
transitions: transitionEvaluations
|
|
2754
|
-
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed))
|
|
2856
|
+
}, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableSlots = await evaluateEditableSlots({
|
|
2857
|
+
instance,
|
|
2858
|
+
definition,
|
|
2859
|
+
stage,
|
|
2860
|
+
actor,
|
|
2861
|
+
snapshot,
|
|
2862
|
+
scope,
|
|
2863
|
+
guardDenial: editGuardDenial
|
|
2864
|
+
});
|
|
2755
2865
|
return {
|
|
2756
2866
|
instance,
|
|
2757
2867
|
definition,
|
|
2758
2868
|
actor,
|
|
2759
2869
|
currentStage,
|
|
2760
2870
|
pendingOnYou,
|
|
2761
|
-
canInteract
|
|
2871
|
+
canInteract,
|
|
2872
|
+
editableSlots
|
|
2762
2873
|
};
|
|
2763
2874
|
}
|
|
2875
|
+
async function evaluateEditableSlots(args) {
|
|
2876
|
+
const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
|
|
2877
|
+
for (const slot of editableSlotsInStage(definition, stage)) {
|
|
2878
|
+
const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
|
|
2879
|
+
slot,
|
|
2880
|
+
window,
|
|
2881
|
+
guardDenial,
|
|
2882
|
+
instance,
|
|
2883
|
+
snapshot,
|
|
2884
|
+
scope,
|
|
2885
|
+
actor,
|
|
2886
|
+
roleAliases: definition.roleAliases
|
|
2887
|
+
}), reason = editDisabledReason({
|
|
2888
|
+
effective: slot.effective,
|
|
2889
|
+
instance,
|
|
2890
|
+
window,
|
|
2891
|
+
guardDenial,
|
|
2892
|
+
predicateSatisfied
|
|
2893
|
+
}), value = readSlotValue(instance, slot);
|
|
2894
|
+
slots.push({
|
|
2895
|
+
scope: slot.scope,
|
|
2896
|
+
...slot.task !== void 0 ? { task: slot.task } : {},
|
|
2897
|
+
name: slot.name,
|
|
2898
|
+
type: slot.type,
|
|
2899
|
+
...slot.title !== void 0 ? { title: slot.title } : {},
|
|
2900
|
+
value,
|
|
2901
|
+
editable: reason === void 0,
|
|
2902
|
+
...reason !== void 0 ? { disabledReason: reason } : {},
|
|
2903
|
+
...slotProvenance(instance, slot.ref)
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
return slots;
|
|
2907
|
+
}
|
|
2908
|
+
async function editPredicateSatisfied(args) {
|
|
2909
|
+
const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
|
|
2910
|
+
if (!window.open || guardDenial !== void 0) return !1;
|
|
2911
|
+
if (slot.effective === !0) return !0;
|
|
2912
|
+
const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
|
|
2913
|
+
return evaluateCondition({ condition: slot.effective, snapshot, params });
|
|
2914
|
+
}
|
|
2764
2915
|
async function renderScope(args) {
|
|
2765
2916
|
const { instance, definition, actor, grants, snapshot, now } = args, params = {
|
|
2766
2917
|
...buildParams({ instance, now, snapshot }),
|
|
@@ -2786,15 +2937,36 @@ async function advisoryCan(instance, actor, grants) {
|
|
|
2786
2937
|
});
|
|
2787
2938
|
return can;
|
|
2788
2939
|
}
|
|
2789
|
-
|
|
2790
|
-
const {
|
|
2940
|
+
function taskScopeFor(args) {
|
|
2941
|
+
const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
|
|
2942
|
+
return {
|
|
2791
2943
|
...scope,
|
|
2792
2944
|
state: {
|
|
2793
2945
|
...scope.state,
|
|
2794
|
-
...scopedStateOverlay(instance, snapshot,
|
|
2946
|
+
...scopedStateOverlay(instance, snapshot, taskName)
|
|
2795
2947
|
},
|
|
2796
|
-
assigned
|
|
2797
|
-
}
|
|
2948
|
+
assigned: assignedFor(instance, taskName, actor, roleAliases)
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
async function evaluateTask(args) {
|
|
2952
|
+
const {
|
|
2953
|
+
task,
|
|
2954
|
+
statusEntry,
|
|
2955
|
+
instance,
|
|
2956
|
+
actor,
|
|
2957
|
+
snapshot,
|
|
2958
|
+
scope,
|
|
2959
|
+
roleAliases,
|
|
2960
|
+
stageHasExits,
|
|
2961
|
+
guardDenial
|
|
2962
|
+
} = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
|
|
2963
|
+
scope,
|
|
2964
|
+
instance,
|
|
2965
|
+
snapshot,
|
|
2966
|
+
actor,
|
|
2967
|
+
taskName: task.name,
|
|
2968
|
+
roleAliases
|
|
2969
|
+
}), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
|
|
2798
2970
|
requirements: task.requirements,
|
|
2799
2971
|
snapshot,
|
|
2800
2972
|
params: taskScope
|
|
@@ -3021,10 +3193,7 @@ async function commitAbort(ctx, reason, actor) {
|
|
|
3021
3193
|
async function fireAction(args) {
|
|
3022
3194
|
const { client, instanceId, task, action, params, options } = args;
|
|
3023
3195
|
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3024
|
-
const ctx = await
|
|
3025
|
-
...options?.clock ? { clock: options.clock } : {},
|
|
3026
|
-
...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
|
|
3027
|
-
});
|
|
3196
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3028
3197
|
try {
|
|
3029
3198
|
return await commitAction(ctx, task, action, params, options);
|
|
3030
3199
|
} catch (error) {
|
|
@@ -3119,6 +3288,114 @@ function formatDisabledReason(task, action, reason) {
|
|
|
3119
3288
|
const detail = disabledReasonDetail[reason.kind](reason);
|
|
3120
3289
|
return `Action "${task}:${action}" is not allowed: ${detail}`;
|
|
3121
3290
|
}
|
|
3291
|
+
class EditStateDeniedError extends Error {
|
|
3292
|
+
reason;
|
|
3293
|
+
target;
|
|
3294
|
+
constructor(args) {
|
|
3295
|
+
super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
const editDisabledReasonDetail = {
|
|
3299
|
+
"not-editable": () => "slot is not declared editable",
|
|
3300
|
+
"instance-completed": (r) => `instance completed at ${r.completedAt}`,
|
|
3301
|
+
"edit-window-closed": (r) => `edit window closed (${r.detail})`,
|
|
3302
|
+
"editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
|
|
3303
|
+
"mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
|
|
3304
|
+
};
|
|
3305
|
+
function formatEditDisabledReason(target, reason) {
|
|
3306
|
+
const detail = editDisabledReasonDetail[reason.kind](
|
|
3307
|
+
reason
|
|
3308
|
+
), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3309
|
+
return `State "${target.scope}:${where}" is not editable: ${detail}`;
|
|
3310
|
+
}
|
|
3311
|
+
async function editState(args) {
|
|
3312
|
+
const { client, instanceId, target, mode = "set", value, options } = args;
|
|
3313
|
+
for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
|
|
3314
|
+
const ctx = await loadCallContext(client, instanceId, options);
|
|
3315
|
+
try {
|
|
3316
|
+
return await commitEdit(ctx, target, mode, value, options);
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
if (!isRevisionConflict(error)) throw error;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
throw new ConcurrentEditStateError({
|
|
3322
|
+
instanceId,
|
|
3323
|
+
target: labelTarget(target),
|
|
3324
|
+
attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
async function commitEdit(ctx, target, mode, value, options) {
|
|
3328
|
+
const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
|
|
3329
|
+
if (slot === void 0)
|
|
3330
|
+
throw new Error(
|
|
3331
|
+
`No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
|
|
3332
|
+
);
|
|
3333
|
+
await assertSlotEditable(ctx, slot, options);
|
|
3334
|
+
const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
|
|
3335
|
+
ops: [op],
|
|
3336
|
+
mutation,
|
|
3337
|
+
stage: stage.name,
|
|
3338
|
+
origin: {
|
|
3339
|
+
edit: !0,
|
|
3340
|
+
...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
|
|
3341
|
+
},
|
|
3342
|
+
params: {},
|
|
3343
|
+
actor,
|
|
3344
|
+
self: selfGdr(ctx.instance),
|
|
3345
|
+
now: ctx.now
|
|
3346
|
+
});
|
|
3347
|
+
return await persistThenDeploy(
|
|
3348
|
+
ctx,
|
|
3349
|
+
mutation,
|
|
3350
|
+
() => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
|
|
3351
|
+
), {
|
|
3352
|
+
edited: !0,
|
|
3353
|
+
target: slotTarget(slot),
|
|
3354
|
+
...ranOps.length > 0 ? { ranOps } : {}
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
async function assertSlotEditable(ctx, slot, options) {
|
|
3358
|
+
const actor = options?.actor, window = slotWindowOpen(ctx.instance, slot), can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0, predicateSatisfied = window.open && slot.effective !== !0 && slot.effective !== void 0 ? await ctxEvaluateCondition(ctx, slot.effective, {
|
|
3359
|
+
...slot.task !== void 0 ? { taskName: slot.task } : {},
|
|
3360
|
+
...actor !== void 0 ? { actor } : {},
|
|
3361
|
+
...can !== void 0 ? { vars: { can } } : {}
|
|
3362
|
+
}) : slot.effective === !0, reason = editDisabledReason({
|
|
3363
|
+
effective: slot.effective,
|
|
3364
|
+
instance: ctx.instance,
|
|
3365
|
+
window,
|
|
3366
|
+
guardDenial: void 0,
|
|
3367
|
+
predicateSatisfied
|
|
3368
|
+
});
|
|
3369
|
+
if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
|
|
3370
|
+
}
|
|
3371
|
+
function buildEditOp(slot, mode, value) {
|
|
3372
|
+
if (mode === "unset") return { type: "state.unset", target: slot.ref };
|
|
3373
|
+
if (value === void 0)
|
|
3374
|
+
throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
|
|
3375
|
+
return {
|
|
3376
|
+
type: mode === "append" ? "state.append" : "state.set",
|
|
3377
|
+
target: slot.ref,
|
|
3378
|
+
value: { type: "literal", value }
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function slotTarget(slot) {
|
|
3382
|
+
return {
|
|
3383
|
+
scope: slot.scope,
|
|
3384
|
+
state: slot.name,
|
|
3385
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
function labelTarget(target) {
|
|
3389
|
+
return {
|
|
3390
|
+
scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
|
|
3391
|
+
state: target.state,
|
|
3392
|
+
...target.task !== void 0 ? { task: target.task } : {}
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function describeTarget(target) {
|
|
3396
|
+
const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
|
|
3397
|
+
return target.scope !== void 0 ? `${target.scope}:${where}` : where;
|
|
3398
|
+
}
|
|
3122
3399
|
function bareIdFromSpawnRef(uri) {
|
|
3123
3400
|
if (!uri.includes(":")) return [uri];
|
|
3124
3401
|
try {
|
|
@@ -3165,18 +3442,60 @@ function buildClientForGdr(defaultClient, resolver) {
|
|
|
3165
3442
|
function toEffectsContextEntries(ctx) {
|
|
3166
3443
|
return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
|
|
3167
3444
|
}
|
|
3445
|
+
async function dispatchGatedWrite(args) {
|
|
3446
|
+
const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
|
|
3447
|
+
client,
|
|
3448
|
+
tag,
|
|
3449
|
+
instanceId,
|
|
3450
|
+
access,
|
|
3451
|
+
clock: args.clock,
|
|
3452
|
+
...resourceClients !== void 0 ? { resourceClients } : {}
|
|
3453
|
+
}), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
|
|
3454
|
+
return {
|
|
3455
|
+
instance: await reload(client, instanceId, tag),
|
|
3456
|
+
cascaded,
|
|
3457
|
+
fired: !0,
|
|
3458
|
+
...ranOps !== void 0 ? { ranOps } : {}
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3168
3461
|
function assertActionAllowed(evaluation, task, action) {
|
|
3169
3462
|
const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
|
|
3170
3463
|
if (actionEval?.allowed === !1 && actionEval.disabledReason)
|
|
3171
3464
|
throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
|
|
3172
3465
|
}
|
|
3173
|
-
|
|
3174
|
-
const
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3466
|
+
function assertEditAllowed(evaluation, target) {
|
|
3467
|
+
const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
|
|
3468
|
+
if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
|
|
3469
|
+
throw new EditStateDeniedError({
|
|
3470
|
+
target: {
|
|
3471
|
+
scope: slot.scope,
|
|
3472
|
+
state: slot.name,
|
|
3473
|
+
...slot.task !== void 0 ? { task: slot.task } : {}
|
|
3474
|
+
},
|
|
3475
|
+
reason: slot.disabledReason
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
async function applyEdit(args) {
|
|
3479
|
+
const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
|
|
3480
|
+
return (await editState({
|
|
3481
|
+
client,
|
|
3482
|
+
instanceId: instance._id,
|
|
3483
|
+
target,
|
|
3484
|
+
...mode !== void 0 ? { mode } : {},
|
|
3485
|
+
...value !== void 0 ? { value } : {},
|
|
3486
|
+
options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
|
|
3487
|
+
})).ranOps;
|
|
3488
|
+
}
|
|
3489
|
+
function buildEngineCallOptions(args) {
|
|
3490
|
+
return {
|
|
3491
|
+
actor: args.actor,
|
|
3492
|
+
...args.grants !== void 0 ? { grants: args.grants } : {},
|
|
3493
|
+
...args.clock !== void 0 ? { clock: args.clock } : {},
|
|
3494
|
+
...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
|
|
3179
3495
|
};
|
|
3496
|
+
}
|
|
3497
|
+
async function applyAction(args) {
|
|
3498
|
+
const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
|
|
3180
3499
|
return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
|
|
3181
3500
|
client,
|
|
3182
3501
|
instanceId: instance._id,
|
|
@@ -3392,34 +3711,68 @@ const workflow = {
|
|
|
3392
3711
|
idempotent,
|
|
3393
3712
|
resourceClients
|
|
3394
3713
|
} = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3395
|
-
|
|
3396
|
-
return { instance: before, cascaded: 0, fired: !1 };
|
|
3397
|
-
const evaluation = await evaluateInstance({
|
|
3714
|
+
return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
|
|
3398
3715
|
client,
|
|
3399
3716
|
tag,
|
|
3400
3717
|
instanceId,
|
|
3401
3718
|
access,
|
|
3719
|
+
actor,
|
|
3720
|
+
clientForGdr,
|
|
3402
3721
|
clock,
|
|
3403
|
-
|
|
3722
|
+
before,
|
|
3723
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3724
|
+
apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
|
|
3725
|
+
client,
|
|
3726
|
+
instance,
|
|
3727
|
+
task,
|
|
3728
|
+
action,
|
|
3729
|
+
actor,
|
|
3730
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3731
|
+
clock,
|
|
3732
|
+
clientForGdr,
|
|
3733
|
+
...params !== void 0 ? { params } : {}
|
|
3734
|
+
}))
|
|
3404
3735
|
});
|
|
3405
|
-
|
|
3406
|
-
|
|
3736
|
+
},
|
|
3737
|
+
/**
|
|
3738
|
+
* Edit a declared-editable state slot directly — reassign, reschedule,
|
|
3739
|
+
* claim-by-hand, append to a running log — through the generic edit seam,
|
|
3740
|
+
* instead of a bespoke action per field. Soft-gates on the slot's declared
|
|
3741
|
+
* editability (the same projection a UI renders), applies the edit as a
|
|
3742
|
+
* `state.*` op (so provenance + history are stamped by the op path),
|
|
3743
|
+
* refreshes the stage's guards, then cascades — an edit to a value a
|
|
3744
|
+
* transition reads can and should move the instance. Advisory like every
|
|
3745
|
+
* engine gate; the lake ACL + guards are the only hard locks.
|
|
3746
|
+
*
|
|
3747
|
+
* Each call is a discrete COMMIT (a history entry, a guard refresh, a
|
|
3748
|
+
* cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
|
|
3749
|
+
* UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
|
|
3750
|
+
* never an `onChange` per keystroke.
|
|
3751
|
+
*/
|
|
3752
|
+
editState: async (args) => {
|
|
3753
|
+
const { client, tag, instanceId, target, mode, value, resourceClients } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
|
|
3754
|
+
return dispatchGatedWrite({
|
|
3407
3755
|
client,
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3756
|
+
tag,
|
|
3757
|
+
instanceId,
|
|
3758
|
+
access,
|
|
3411
3759
|
actor,
|
|
3412
|
-
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3413
|
-
clock,
|
|
3414
3760
|
clientForGdr,
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3761
|
+
clock,
|
|
3762
|
+
before,
|
|
3763
|
+
...resourceClients !== void 0 ? { resourceClients } : {},
|
|
3764
|
+
apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
|
|
3765
|
+
client,
|
|
3766
|
+
instance,
|
|
3767
|
+
target,
|
|
3768
|
+
...mode !== void 0 ? { mode } : {},
|
|
3769
|
+
...value !== void 0 ? { value } : {},
|
|
3770
|
+
actor,
|
|
3771
|
+
...access.grants !== void 0 ? { grants: access.grants } : {},
|
|
3772
|
+
clock,
|
|
3773
|
+
clientForGdr
|
|
3774
|
+
}))
|
|
3775
|
+
});
|
|
3423
3776
|
},
|
|
3424
3777
|
/**
|
|
3425
3778
|
* Report a queued effect's outcome. Drains it from `pendingEffects`,
|
|
@@ -3837,6 +4190,9 @@ function createInstanceSession(args) {
|
|
|
3837
4190
|
...clock !== void 0 ? { now: clock() } : {},
|
|
3838
4191
|
...grants !== void 0 ? { grants } : {}
|
|
3839
4192
|
});
|
|
4193
|
+
}, settleAfterApply = async (actor, held, ranOps) => {
|
|
4194
|
+
const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
|
|
4195
|
+
return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
|
|
3840
4196
|
}, commit = async (run) => {
|
|
3841
4197
|
committing = !0;
|
|
3842
4198
|
const held = new Map(overlay);
|
|
@@ -3887,8 +4243,25 @@ function createInstanceSession(args) {
|
|
|
3887
4243
|
...grants !== void 0 ? { grants } : {},
|
|
3888
4244
|
...clock !== void 0 ? { clock } : {},
|
|
3889
4245
|
...params !== void 0 ? { params } : {}
|
|
3890
|
-
})
|
|
3891
|
-
return
|
|
4246
|
+
});
|
|
4247
|
+
return settleAfterApply(actor, held, ranOps);
|
|
4248
|
+
});
|
|
4249
|
+
},
|
|
4250
|
+
editState({ target, mode, value }) {
|
|
4251
|
+
return commit(async (actor, held) => {
|
|
4252
|
+
assertEditAllowed(await evaluateWith(held, heldGuards), target);
|
|
4253
|
+
const { grants } = await access(), ranOps = await applyEdit({
|
|
4254
|
+
client,
|
|
4255
|
+
instance,
|
|
4256
|
+
target,
|
|
4257
|
+
...mode !== void 0 ? { mode } : {},
|
|
4258
|
+
...value !== void 0 ? { value } : {},
|
|
4259
|
+
actor,
|
|
4260
|
+
clientForGdr,
|
|
4261
|
+
...grants !== void 0 ? { grants } : {},
|
|
4262
|
+
...clock !== void 0 ? { clock } : {}
|
|
4263
|
+
});
|
|
4264
|
+
return settleAfterApply(actor, held, ranOps);
|
|
3892
4265
|
});
|
|
3893
4266
|
}
|
|
3894
4267
|
};
|
|
@@ -3936,6 +4309,7 @@ function createEngine(args) {
|
|
|
3936
4309
|
deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
|
|
3937
4310
|
startInstance: (rest) => workflow.startInstance(bind(rest)),
|
|
3938
4311
|
fireAction: (rest) => workflow.fireAction(bind(rest)),
|
|
4312
|
+
editState: (rest) => workflow.editState(bind(rest)),
|
|
3939
4313
|
completeEffect: (rest) => workflow.completeEffect(bind(rest)),
|
|
3940
4314
|
tick: (rest) => workflow.tick(bind(rest)),
|
|
3941
4315
|
evaluateInstance: (rest) => evaluateInstance(bind(rest)),
|
|
@@ -4232,10 +4606,12 @@ exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
|
|
|
4232
4606
|
exports.ActionDisabledError = ActionDisabledError;
|
|
4233
4607
|
exports.ActionParamsInvalidError = ActionParamsInvalidError;
|
|
4234
4608
|
exports.CascadeLimitError = CascadeLimitError;
|
|
4609
|
+
exports.ConcurrentEditStateError = ConcurrentEditStateError;
|
|
4235
4610
|
exports.ConcurrentFireActionError = ConcurrentFireActionError;
|
|
4236
4611
|
exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
|
|
4237
4612
|
exports.DISPLAY = DISPLAY;
|
|
4238
4613
|
exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
|
|
4614
|
+
exports.EditStateDeniedError = EditStateDeniedError;
|
|
4239
4615
|
exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
|
|
4240
4616
|
exports.GUARD_LIFTED_PREDICATE = GUARD_LIFTED_PREDICATE;
|
|
4241
4617
|
exports.GUARD_OWNER = GUARD_OWNER;
|