@sanity/workflow-engine 0.8.0 → 0.10.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/define.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as v from "valibot";
2
- import { GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringTaskSchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringStateEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
2
+ import { andConditions, expandRequiredRoles, GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringTaskSchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
3
3
  function groq(strings, ...values) {
4
4
  return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
5
5
  }
@@ -12,16 +12,26 @@ function serializeGroqValue(value) {
12
12
  return serialized;
13
13
  }
14
14
  function desugarWorkflow(authoring) {
15
- const ctx = { issues: [], claimStates: /* @__PURE__ */ new Map(), claimedStates: /* @__PURE__ */ new Set() }, workflowState = desugarStateEntries(authoring.state, ["state"], ctx), workflowLayer = layerOf(workflowState), stages = authoring.stages.map((stage, i) => {
16
- const path = ["stages", i], stageState = desugarStateEntries(stage.state, [...path, "state"], ctx), stageEnv = {
15
+ const ctx = {
16
+ issues: [],
17
+ claimFields: /* @__PURE__ */ new Map(),
18
+ claimedFields: /* @__PURE__ */ new Set(),
19
+ roleAliases: authoring.roleAliases
20
+ }, workflowFields = desugarFieldEntries(authoring.fields, ["fields"], ctx), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
21
+ const path = ["stages", i], stageFields = desugarFieldEntries(stage.fields, [...path, "fields"], ctx), stageEnv = {
17
22
  layers: [
18
- { scope: "stage", entries: layerOf(stageState) },
23
+ { scope: "stage", entries: layerOf(stageFields) },
19
24
  { scope: "workflow", entries: workflowLayer }
20
25
  ]
21
26
  }, tasks = (stage.tasks ?? []).map(
22
27
  (task, j) => desugarTask(task, [...path, "tasks", j], stageEnv, ctx)
23
28
  ), transitions = (stage.transitions ?? []).map(
24
29
  (transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
30
+ ), editable = desugarStageEditable(
31
+ stage.editable,
32
+ editableSlotNames(workflowFields, stageFields, tasks),
33
+ [...path, "editable"],
34
+ ctx
25
35
  );
26
36
  return {
27
37
  ...stripUndefined({
@@ -30,12 +40,13 @@ function desugarWorkflow(authoring) {
30
40
  description: stage.description,
31
41
  guards: stage.guards
32
42
  }),
33
- ...stageState ? { state: stageState } : {},
43
+ ...stageFields ? { fields: stageFields } : {},
34
44
  ...tasks.length > 0 ? { tasks } : {},
35
- ...transitions.length > 0 ? { transitions } : {}
45
+ ...transitions.length > 0 ? { transitions } : {},
46
+ ...editable ? { editable } : {}
36
47
  };
37
48
  });
38
- return checkUnclaimedClaimStates(ctx), { definition: {
49
+ return checkUnclaimedClaimFields(ctx), { definition: {
39
50
  ...stripUndefined({
40
51
  name: authoring.name,
41
52
  version: authoring.version,
@@ -43,29 +54,82 @@ function desugarWorkflow(authoring) {
43
54
  description: authoring.description,
44
55
  role: authoring.role,
45
56
  initialStage: authoring.initialStage,
46
- predicates: authoring.predicates
57
+ predicates: authoring.predicates,
58
+ roleAliases: authoring.roleAliases
47
59
  }),
48
- ...workflowState ? { state: workflowState } : {},
60
+ ...workflowFields ? { fields: workflowFields } : {},
49
61
  stages
50
62
  }, issues: ctx.issues };
51
63
  }
52
- function desugarStateEntries(entries, path, ctx) {
64
+ function desugarFieldEntries(entries, path, ctx) {
53
65
  return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
54
- if (entry.type !== "claim") return entry;
55
- const desugared = {
56
- ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
57
- type: "value.actor",
58
- source: { type: "write" }
66
+ if (entry.type === "claim") {
67
+ const desugared = {
68
+ ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
69
+ type: "value.actor",
70
+ source: { type: "write" }
71
+ };
72
+ return ctx.claimFields.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
73
+ }
74
+ const editable = normalizeEditable(entry.editable, [...path, i, "editable"], ctx);
75
+ return {
76
+ ...stripUndefined({
77
+ type: entry.type,
78
+ name: entry.name,
79
+ title: entry.title,
80
+ description: entry.description,
81
+ required: entry.required,
82
+ editable
83
+ }),
84
+ source: entry.source
59
85
  };
60
- return ctx.claimStates.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
61
86
  });
62
87
  }
88
+ function normalizeEditable(editable, path, ctx) {
89
+ if (editable === void 0 || editable === !0) return editable;
90
+ if (Array.isArray(editable)) {
91
+ const condition = rolesCondition(editable, ctx.roleAliases);
92
+ if (condition === void 0) {
93
+ ctx.issues.push({
94
+ path,
95
+ message: "editable: [] names no roles \u2014 use `true` to open the slot to anyone in its scope, or list at least one role"
96
+ });
97
+ return;
98
+ }
99
+ return condition;
100
+ }
101
+ return editable;
102
+ }
103
+ function editableSlotNames(workflowFields, stageFields, tasks) {
104
+ const names = /* @__PURE__ */ new Set(), collect = (entries) => {
105
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
106
+ };
107
+ collect(workflowFields), collect(stageFields);
108
+ for (const task of tasks) collect(task.fields);
109
+ return names;
110
+ }
111
+ function desugarStageEditable(overrides, inScope, path, ctx) {
112
+ if (overrides === void 0) return;
113
+ const out = {};
114
+ for (const [name, value] of Object.entries(overrides)) {
115
+ if (!inScope.has(name)) {
116
+ ctx.issues.push({
117
+ path: [...path, name],
118
+ message: `stage editable override "${name}" does not narrow an editable slot in scope \u2014 name a workflow/stage/task slot of this stage that declares \`editable\``
119
+ });
120
+ continue;
121
+ }
122
+ const normalized = normalizeEditable(value, [...path, name], ctx);
123
+ normalized !== void 0 && (out[name] = normalized);
124
+ }
125
+ return Object.keys(out).length > 0 ? out : void 0;
126
+ }
63
127
  function layerOf(entries) {
64
128
  return new Map((entries ?? []).map((entry) => [entry.name, entry]));
65
129
  }
66
130
  function desugarTask(task, path, stageEnv, ctx) {
67
- const taskState = desugarStateEntries(task.state, [...path, "state"], ctx), env = {
68
- layers: [{ scope: "task", entries: layerOf(taskState) }, ...stageEnv.layers]
131
+ const taskFields = desugarFieldEntries(task.fields, [...path, "fields"], ctx), env = {
132
+ layers: [{ scope: "task", entries: layerOf(taskFields) }, ...stageEnv.layers]
69
133
  }, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
70
134
  (action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
71
135
  );
@@ -82,7 +146,7 @@ function desugarTask(task, path, stageEnv, ctx) {
82
146
  subworkflows: task.subworkflows
83
147
  }),
84
148
  activation: task.activation ?? "manual",
85
- ...taskState ? { state: taskState } : {},
149
+ ...taskFields ? { fields: taskFields } : {},
86
150
  ...ops ? { ops } : {},
87
151
  ...actions.length > 0 ? { actions } : {}
88
152
  };
@@ -90,7 +154,7 @@ function desugarTask(task, path, stageEnv, ctx) {
90
154
  function desugarAction(action, path, env, taskName, ctx) {
91
155
  if ("type" in action)
92
156
  return desugarClaimAction(action, path, env, ctx);
93
- const filter = composeFilter([rolesCondition(action.roles), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
157
+ const filter = andConditions([rolesCondition(action.roles, ctx.roleAliases), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
94
158
  return action.status !== void 0 && ops.push({ type: "status.set", task: taskName, status: action.status }), {
95
159
  ...stripUndefined({
96
160
  name: action.name,
@@ -104,15 +168,19 @@ function desugarAction(action, path, env, taskName, ctx) {
104
168
  };
105
169
  }
106
170
  function desugarClaimAction(action, path, env, ctx) {
107
- const ref = typeof action.state == "string" ? { state: action.state } : action.state, resolved = resolveRef(ref, env, [...path, "state"], ctx);
171
+ const ref = typeof action.field == "string" ? { field: action.field } : action.field, resolved = resolveRef(ref, env, [...path, "field"], ctx);
108
172
  if (resolved) {
109
173
  const entry = entryAt(env, resolved);
110
174
  entry && entry.type !== "value.actor" && ctx.issues.push({
111
- path: [...path, "state"],
112
- message: `claim action "${action.name}" references "${resolved.state}" of kind "${entry.type}" \u2014 a claim pair needs an actor-valued entry (a "claim" or "value.actor" state declaration)`
113
- }), checkShadowedClaimTarget(action.name, ref.state, resolved, env, [...path, "state"], ctx), entry && ctx.claimedStates.add(entry);
175
+ path: [...path, "field"],
176
+ message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" \u2014 a claim pair needs an actor-valued entry (a "claim" or "value.actor" field declaration)`
177
+ }), checkShadowedClaimTarget(action.name, ref.field, resolved, env, [...path, "field"], ctx), entry && ctx.claimedFields.add(entry);
114
178
  }
115
- const noSteal = `!defined($state.${ref.state})`, filter = composeFilter([noSteal, rolesCondition(action.roles), action.filter]), ops = resolved ? [{ type: "state.set", target: resolved, value: { type: "actor" } }] : [];
179
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([
180
+ noSteal,
181
+ rolesCondition(action.roles, ctx.roleAliases),
182
+ action.filter
183
+ ]), ops = resolved ? [{ type: "field.set", target: resolved, value: { type: "actor" } }] : [];
116
184
  return {
117
185
  ...stripUndefined({
118
186
  name: action.name,
@@ -125,18 +193,18 @@ function desugarClaimAction(action, path, env, ctx) {
125
193
  ...ops.length > 0 ? { ops } : {}
126
194
  };
127
195
  }
128
- function checkShadowedClaimTarget(actionName, state, resolved, env, path, ctx) {
129
- const nearest = env.layers.find((l) => l.entries.has(state));
196
+ function checkShadowedClaimTarget(actionName, field, resolved, env, path, ctx) {
197
+ const nearest = env.layers.find((l) => l.entries.has(field));
130
198
  nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
131
199
  path,
132
- message: `claim action "${actionName}" targets "${state}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $state \u2014 the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
200
+ message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields \u2014 the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
133
201
  });
134
202
  }
135
- function checkUnclaimedClaimStates(ctx) {
136
- for (const [entry, { name, path }] of ctx.claimStates)
137
- ctx.claimedStates.has(entry) || ctx.issues.push({
203
+ function checkUnclaimedClaimFields(ctx) {
204
+ for (const [entry, { name, path }] of ctx.claimFields)
205
+ ctx.claimedFields.has(entry) || ctx.issues.push({
138
206
  path,
139
- message: `claim state "${name}" is never referenced by a claim action \u2014 you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", state: "${name}" }) or use a raw value.actor entry`
207
+ message: `claim field "${name}" is never referenced by a claim action \u2014 you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw value.actor entry`
140
208
  });
141
209
  }
142
210
  function desugarTransition(transition, path, stageEnv, ctx) {
@@ -176,7 +244,7 @@ function desugarAuditOp(op, path, env, ctx) {
176
244
  return ctx.issues.push({
177
245
  path: [...path, "value"],
178
246
  message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
179
- }), { type: "state.append", target, value: op.value };
247
+ }), { type: "field.append", target, value: op.value };
180
248
  const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
181
249
  for (const [stamp, source] of [
182
250
  [actorField, AUDIT_STAMPS.actor],
@@ -191,32 +259,27 @@ function desugarAuditOp(op, path, env, ctx) {
191
259
  }
192
260
  fields[stamp] = source;
193
261
  }
194
- return { type: "state.append", target, value: { type: "object", fields } };
262
+ return { type: "field.append", target, value: { type: "object", fields } };
195
263
  }
196
264
  function resolveRef(ref, env, path, ctx) {
197
265
  const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
198
266
  for (const layer of layers)
199
- if (layer.entries.has(ref.state)) return { scope: layer.scope, state: ref.state };
267
+ if (layer.entries.has(ref.field)) return { scope: layer.scope, field: ref.field };
200
268
  const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
201
269
  ctx.issues.push({
202
270
  path,
203
- message: `state reference "${ref.state}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
271
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
204
272
  });
205
273
  }
206
274
  function entryAt(env, ref) {
207
- return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.state);
275
+ return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.field);
208
276
  }
209
277
  function fallbackRef(ref) {
210
- return { scope: ref.scope ?? "workflow", state: ref.state };
278
+ return { scope: ref.scope ?? "workflow", field: ref.field };
211
279
  }
212
- function rolesCondition(roles) {
280
+ function rolesCondition(roles, aliases) {
213
281
  if (!(!roles || roles.length === 0))
214
- return groq`count($actor.roles[@ in ${roles}]) > 0`;
215
- }
216
- function composeFilter(parts) {
217
- const present = parts.filter((p) => p !== void 0);
218
- if (present.length !== 0)
219
- return present.length === 1 ? present[0] : present.map((p) => `(${p})`).join(" && ");
282
+ return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
220
283
  }
221
284
  function stripUndefined(obj) {
222
285
  const out = {};
@@ -225,7 +288,7 @@ function stripUndefined(obj) {
225
288
  return out;
226
289
  }
227
290
  const RESERVED_CONDITION_VARS = [
228
- "state",
291
+ "fields",
229
292
  "actor",
230
293
  "assigned",
231
294
  "can",
@@ -346,43 +409,43 @@ function checkGuardNames(def, issues) {
346
409
  issues
347
410
  );
348
411
  }
349
- function stateScopes(def) {
412
+ function fieldScopes(def) {
350
413
  const scopes = [
351
- { entries: def.state, scope: "workflow", path: ["state"], label: "workflow state" }
414
+ { entries: def.fields, scope: "workflow", path: ["fields"], label: "workflow fields" }
352
415
  ];
353
416
  for (const [i, stage] of def.stages.entries()) {
354
417
  scopes.push({
355
- entries: stage.state,
418
+ entries: stage.fields,
356
419
  scope: "stage",
357
- path: ["stages", i, "state"],
358
- label: `stage "${stage.name}" state`
420
+ path: ["stages", i, "fields"],
421
+ label: `stage "${stage.name}" fields`
359
422
  });
360
423
  for (const [j, task] of (stage.tasks ?? []).entries())
361
424
  scopes.push({
362
- entries: task.state,
425
+ entries: task.fields,
363
426
  scope: "task",
364
- path: ["stages", i, "tasks", j, "state"],
365
- label: `task "${task.name}" state`
427
+ path: ["stages", i, "tasks", j, "fields"],
428
+ label: `task "${task.name}" fields`
366
429
  });
367
430
  }
368
431
  return scopes;
369
432
  }
370
- function checkRequiredState(def, issues) {
371
- for (const { entries, scope, path, label } of stateScopes(def))
433
+ function checkRequiredField(def, issues) {
434
+ for (const { entries, scope, path, label } of fieldScopes(def))
372
435
  for (const [n, entry] of (entries ?? []).entries())
373
436
  entry.required === !0 && (scope !== "workflow" ? issues.push({
374
437
  path: [...path, n, "required"],
375
438
  message: `${label} entry "${entry.name}" is \`required\`, but \`required\` applies only to workflow-scope \`init\` entries \u2014 only those are caller-supplied at start/spawn`
376
439
  }) : entry.source.type !== "init" && issues.push({
377
440
  path: [...path, n, "required"],
378
- message: `state entry "${entry.name}" is \`required\` but its source is "${entry.source.type}" \u2014 \`required\` applies only to \`init\`-sourced entries (the caller fills them at start/spawn)`
441
+ message: `field entry "${entry.name}" is \`required\` but its source is "${entry.source.type}" \u2014 \`required\` applies only to \`init\`-sourced entries (the caller fills them at start/spawn)`
379
442
  }));
380
443
  }
381
- function checkStateEntryNames(def, issues) {
382
- for (const { entries, path, label } of stateScopes(def))
444
+ function checkFieldEntryNames(def, issues) {
445
+ for (const { entries, path, label } of fieldScopes(def))
383
446
  checkDuplicates(
384
447
  (entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
385
- `state entry name in ${label}`,
448
+ `field entry name in ${label}`,
386
449
  issues
387
450
  );
388
451
  }
@@ -482,15 +545,15 @@ function collectBindingSites(effects, path, label, sites) {
482
545
  });
483
546
  }
484
547
  function checkAssigneesEntries(def, issues) {
485
- for (const { entries, path } of stateScopes(def))
548
+ for (const { entries, path } of fieldScopes(def))
486
549
  (entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
487
550
  path,
488
- message: "at most one assignees-kind state entry per scope \u2014 the inbox reads it by kind"
551
+ message: "at most one assignees-kind field entry per scope \u2014 the inbox reads it by kind"
489
552
  });
490
553
  }
491
554
  function checkWorkflowInvariants(def) {
492
555
  const issues = [], stageNames = checkStages(def, issues);
493
- return checkInitialStage(def, stageNames, issues), checkTransitionTargets(def, stageNames, issues), checkEffectNames(def, issues), checkGuardNames(def, issues), checkStateEntryNames(def, issues), checkRequiredState(def, issues), checkPredicates(def, issues), checkCanOutsideActionFilters(def, issues), checkAssigneesEntries(def, issues), issues;
556
+ return checkInitialStage(def, stageNames, issues), checkTransitionTargets(def, stageNames, issues), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues), checkRequiredField(def, issues), checkPredicates(def, issues), checkCanOutsideActionFilters(def, issues), checkAssigneesEntries(def, issues), issues;
494
557
  }
495
558
  function defineWorkflow(definition) {
496
559
  const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
@@ -509,8 +572,8 @@ function defineAction(action) {
509
572
  function defineTransition(transition) {
510
573
  return parseOrThrow(AuthoringTransitionSchema, transition, transitionLabel(transition));
511
574
  }
512
- function defineState(entry) {
513
- return parseOrThrow(AuthoringStateEntrySchema, entry, labelFor("defineState", entry));
575
+ function defineField(entry) {
576
+ return parseOrThrow(AuthoringFieldEntrySchema, entry, labelFor("defineField", entry));
514
577
  }
515
578
  function defineOp(op) {
516
579
  return parseOrThrow(AuthoringOpSchema, op, "defineOp");
@@ -546,10 +609,10 @@ export {
546
609
  defineAction,
547
610
  defineEffect,
548
611
  defineEffectDescriptor,
612
+ defineField,
549
613
  defineGuard,
550
614
  defineOp,
551
615
  defineStage,
552
- defineState,
553
616
  defineTask,
554
617
  defineTransition,
555
618
  defineWorkflow,