@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.cjs CHANGED
@@ -29,16 +29,26 @@ function serializeGroqValue(value) {
29
29
  return serialized;
30
30
  }
31
31
  function desugarWorkflow(authoring) {
32
- 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) => {
33
- const path = ["stages", i], stageState = desugarStateEntries(stage.state, [...path, "state"], ctx), stageEnv = {
32
+ const ctx = {
33
+ issues: [],
34
+ claimFields: /* @__PURE__ */ new Map(),
35
+ claimedFields: /* @__PURE__ */ new Set(),
36
+ roleAliases: authoring.roleAliases
37
+ }, workflowFields = desugarFieldEntries(authoring.fields, ["fields"], ctx), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
38
+ const path = ["stages", i], stageFields = desugarFieldEntries(stage.fields, [...path, "fields"], ctx), stageEnv = {
34
39
  layers: [
35
- { scope: "stage", entries: layerOf(stageState) },
40
+ { scope: "stage", entries: layerOf(stageFields) },
36
41
  { scope: "workflow", entries: workflowLayer }
37
42
  ]
38
43
  }, tasks = (stage.tasks ?? []).map(
39
44
  (task, j) => desugarTask(task, [...path, "tasks", j], stageEnv, ctx)
40
45
  ), transitions = (stage.transitions ?? []).map(
41
46
  (transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
47
+ ), editable = desugarStageEditable(
48
+ stage.editable,
49
+ editableSlotNames(workflowFields, stageFields, tasks),
50
+ [...path, "editable"],
51
+ ctx
42
52
  );
43
53
  return {
44
54
  ...stripUndefined({
@@ -47,12 +57,13 @@ function desugarWorkflow(authoring) {
47
57
  description: stage.description,
48
58
  guards: stage.guards
49
59
  }),
50
- ...stageState ? { state: stageState } : {},
60
+ ...stageFields ? { fields: stageFields } : {},
51
61
  ...tasks.length > 0 ? { tasks } : {},
52
- ...transitions.length > 0 ? { transitions } : {}
62
+ ...transitions.length > 0 ? { transitions } : {},
63
+ ...editable ? { editable } : {}
53
64
  };
54
65
  });
55
- return checkUnclaimedClaimStates(ctx), { definition: {
66
+ return checkUnclaimedClaimFields(ctx), { definition: {
56
67
  ...stripUndefined({
57
68
  name: authoring.name,
58
69
  version: authoring.version,
@@ -60,29 +71,82 @@ function desugarWorkflow(authoring) {
60
71
  description: authoring.description,
61
72
  role: authoring.role,
62
73
  initialStage: authoring.initialStage,
63
- predicates: authoring.predicates
74
+ predicates: authoring.predicates,
75
+ roleAliases: authoring.roleAliases
64
76
  }),
65
- ...workflowState ? { state: workflowState } : {},
77
+ ...workflowFields ? { fields: workflowFields } : {},
66
78
  stages
67
79
  }, issues: ctx.issues };
68
80
  }
69
- function desugarStateEntries(entries, path, ctx) {
81
+ function desugarFieldEntries(entries, path, ctx) {
70
82
  return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
71
- if (entry.type !== "claim") return entry;
72
- const desugared = {
73
- ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
74
- type: "value.actor",
75
- source: { type: "write" }
83
+ if (entry.type === "claim") {
84
+ const desugared = {
85
+ ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
86
+ type: "value.actor",
87
+ source: { type: "write" }
88
+ };
89
+ return ctx.claimFields.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
90
+ }
91
+ const editable = normalizeEditable(entry.editable, [...path, i, "editable"], ctx);
92
+ return {
93
+ ...stripUndefined({
94
+ type: entry.type,
95
+ name: entry.name,
96
+ title: entry.title,
97
+ description: entry.description,
98
+ required: entry.required,
99
+ editable
100
+ }),
101
+ source: entry.source
76
102
  };
77
- return ctx.claimStates.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
78
103
  });
79
104
  }
105
+ function normalizeEditable(editable, path, ctx) {
106
+ if (editable === void 0 || editable === !0) return editable;
107
+ if (Array.isArray(editable)) {
108
+ const condition = rolesCondition(editable, ctx.roleAliases);
109
+ if (condition === void 0) {
110
+ ctx.issues.push({
111
+ path,
112
+ message: "editable: [] names no roles \u2014 use `true` to open the slot to anyone in its scope, or list at least one role"
113
+ });
114
+ return;
115
+ }
116
+ return condition;
117
+ }
118
+ return editable;
119
+ }
120
+ function editableSlotNames(workflowFields, stageFields, tasks) {
121
+ const names = /* @__PURE__ */ new Set(), collect = (entries) => {
122
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
123
+ };
124
+ collect(workflowFields), collect(stageFields);
125
+ for (const task of tasks) collect(task.fields);
126
+ return names;
127
+ }
128
+ function desugarStageEditable(overrides, inScope, path, ctx) {
129
+ if (overrides === void 0) return;
130
+ const out = {};
131
+ for (const [name, value] of Object.entries(overrides)) {
132
+ if (!inScope.has(name)) {
133
+ ctx.issues.push({
134
+ path: [...path, name],
135
+ 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\``
136
+ });
137
+ continue;
138
+ }
139
+ const normalized = normalizeEditable(value, [...path, name], ctx);
140
+ normalized !== void 0 && (out[name] = normalized);
141
+ }
142
+ return Object.keys(out).length > 0 ? out : void 0;
143
+ }
80
144
  function layerOf(entries) {
81
145
  return new Map((entries ?? []).map((entry) => [entry.name, entry]));
82
146
  }
83
147
  function desugarTask(task, path, stageEnv, ctx) {
84
- const taskState = desugarStateEntries(task.state, [...path, "state"], ctx), env = {
85
- layers: [{ scope: "task", entries: layerOf(taskState) }, ...stageEnv.layers]
148
+ const taskFields = desugarFieldEntries(task.fields, [...path, "fields"], ctx), env = {
149
+ layers: [{ scope: "task", entries: layerOf(taskFields) }, ...stageEnv.layers]
86
150
  }, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
87
151
  (action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
88
152
  );
@@ -99,7 +163,7 @@ function desugarTask(task, path, stageEnv, ctx) {
99
163
  subworkflows: task.subworkflows
100
164
  }),
101
165
  activation: task.activation ?? "manual",
102
- ...taskState ? { state: taskState } : {},
166
+ ...taskFields ? { fields: taskFields } : {},
103
167
  ...ops ? { ops } : {},
104
168
  ...actions.length > 0 ? { actions } : {}
105
169
  };
@@ -107,7 +171,7 @@ function desugarTask(task, path, stageEnv, ctx) {
107
171
  function desugarAction(action, path, env, taskName, ctx) {
108
172
  if ("type" in action)
109
173
  return desugarClaimAction(action, path, env, ctx);
110
- const filter = composeFilter([rolesCondition(action.roles), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
174
+ const filter = schema.andConditions([rolesCondition(action.roles, ctx.roleAliases), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
111
175
  return action.status !== void 0 && ops.push({ type: "status.set", task: taskName, status: action.status }), {
112
176
  ...stripUndefined({
113
177
  name: action.name,
@@ -121,15 +185,19 @@ function desugarAction(action, path, env, taskName, ctx) {
121
185
  };
122
186
  }
123
187
  function desugarClaimAction(action, path, env, ctx) {
124
- const ref = typeof action.state == "string" ? { state: action.state } : action.state, resolved = resolveRef(ref, env, [...path, "state"], ctx);
188
+ const ref = typeof action.field == "string" ? { field: action.field } : action.field, resolved = resolveRef(ref, env, [...path, "field"], ctx);
125
189
  if (resolved) {
126
190
  const entry = entryAt(env, resolved);
127
191
  entry && entry.type !== "value.actor" && ctx.issues.push({
128
- path: [...path, "state"],
129
- 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)`
130
- }), checkShadowedClaimTarget(action.name, ref.state, resolved, env, [...path, "state"], ctx), entry && ctx.claimedStates.add(entry);
192
+ path: [...path, "field"],
193
+ 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)`
194
+ }), checkShadowedClaimTarget(action.name, ref.field, resolved, env, [...path, "field"], ctx), entry && ctx.claimedFields.add(entry);
131
195
  }
132
- const noSteal = `!defined($state.${ref.state})`, filter = composeFilter([noSteal, rolesCondition(action.roles), action.filter]), ops = resolved ? [{ type: "state.set", target: resolved, value: { type: "actor" } }] : [];
196
+ const noSteal = `!defined($fields.${ref.field})`, filter = schema.andConditions([
197
+ noSteal,
198
+ rolesCondition(action.roles, ctx.roleAliases),
199
+ action.filter
200
+ ]), ops = resolved ? [{ type: "field.set", target: resolved, value: { type: "actor" } }] : [];
133
201
  return {
134
202
  ...stripUndefined({
135
203
  name: action.name,
@@ -142,18 +210,18 @@ function desugarClaimAction(action, path, env, ctx) {
142
210
  ...ops.length > 0 ? { ops } : {}
143
211
  };
144
212
  }
145
- function checkShadowedClaimTarget(actionName, state, resolved, env, path, ctx) {
146
- const nearest = env.layers.find((l) => l.entries.has(state));
213
+ function checkShadowedClaimTarget(actionName, field, resolved, env, path, ctx) {
214
+ const nearest = env.layers.find((l) => l.entries.has(field));
147
215
  nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
148
216
  path,
149
- 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`
217
+ 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`
150
218
  });
151
219
  }
152
- function checkUnclaimedClaimStates(ctx) {
153
- for (const [entry, { name, path }] of ctx.claimStates)
154
- ctx.claimedStates.has(entry) || ctx.issues.push({
220
+ function checkUnclaimedClaimFields(ctx) {
221
+ for (const [entry, { name, path }] of ctx.claimFields)
222
+ ctx.claimedFields.has(entry) || ctx.issues.push({
155
223
  path,
156
- 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`
224
+ 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`
157
225
  });
158
226
  }
159
227
  function desugarTransition(transition, path, stageEnv, ctx) {
@@ -193,7 +261,7 @@ function desugarAuditOp(op, path, env, ctx) {
193
261
  return ctx.issues.push({
194
262
  path: [...path, "value"],
195
263
  message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
196
- }), { type: "state.append", target, value: op.value };
264
+ }), { type: "field.append", target, value: op.value };
197
265
  const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
198
266
  for (const [stamp, source] of [
199
267
  [actorField, AUDIT_STAMPS.actor],
@@ -208,32 +276,27 @@ function desugarAuditOp(op, path, env, ctx) {
208
276
  }
209
277
  fields[stamp] = source;
210
278
  }
211
- return { type: "state.append", target, value: { type: "object", fields } };
279
+ return { type: "field.append", target, value: { type: "object", fields } };
212
280
  }
213
281
  function resolveRef(ref, env, path, ctx) {
214
282
  const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
215
283
  for (const layer of layers)
216
- if (layer.entries.has(ref.state)) return { scope: layer.scope, state: ref.state };
284
+ if (layer.entries.has(ref.field)) return { scope: layer.scope, field: ref.field };
217
285
  const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
218
286
  ctx.issues.push({
219
287
  path,
220
- message: `state reference "${ref.state}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
288
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
221
289
  });
222
290
  }
223
291
  function entryAt(env, ref) {
224
- return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.state);
292
+ return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.field);
225
293
  }
226
294
  function fallbackRef(ref) {
227
- return { scope: ref.scope ?? "workflow", state: ref.state };
295
+ return { scope: ref.scope ?? "workflow", field: ref.field };
228
296
  }
229
- function rolesCondition(roles) {
297
+ function rolesCondition(roles, aliases) {
230
298
  if (!(!roles || roles.length === 0))
231
- return groq`count($actor.roles[@ in ${roles}]) > 0`;
232
- }
233
- function composeFilter(parts) {
234
- const present = parts.filter((p) => p !== void 0);
235
- if (present.length !== 0)
236
- return present.length === 1 ? present[0] : present.map((p) => `(${p})`).join(" && ");
299
+ return groq`count($actor.roles[@ in ${schema.expandRequiredRoles(roles, aliases)}]) > 0`;
237
300
  }
238
301
  function stripUndefined(obj) {
239
302
  const out = {};
@@ -242,7 +305,7 @@ function stripUndefined(obj) {
242
305
  return out;
243
306
  }
244
307
  const RESERVED_CONDITION_VARS = [
245
- "state",
308
+ "fields",
246
309
  "actor",
247
310
  "assigned",
248
311
  "can",
@@ -363,43 +426,43 @@ function checkGuardNames(def, issues) {
363
426
  issues
364
427
  );
365
428
  }
366
- function stateScopes(def) {
429
+ function fieldScopes(def) {
367
430
  const scopes = [
368
- { entries: def.state, scope: "workflow", path: ["state"], label: "workflow state" }
431
+ { entries: def.fields, scope: "workflow", path: ["fields"], label: "workflow fields" }
369
432
  ];
370
433
  for (const [i, stage] of def.stages.entries()) {
371
434
  scopes.push({
372
- entries: stage.state,
435
+ entries: stage.fields,
373
436
  scope: "stage",
374
- path: ["stages", i, "state"],
375
- label: `stage "${stage.name}" state`
437
+ path: ["stages", i, "fields"],
438
+ label: `stage "${stage.name}" fields`
376
439
  });
377
440
  for (const [j, task] of (stage.tasks ?? []).entries())
378
441
  scopes.push({
379
- entries: task.state,
442
+ entries: task.fields,
380
443
  scope: "task",
381
- path: ["stages", i, "tasks", j, "state"],
382
- label: `task "${task.name}" state`
444
+ path: ["stages", i, "tasks", j, "fields"],
445
+ label: `task "${task.name}" fields`
383
446
  });
384
447
  }
385
448
  return scopes;
386
449
  }
387
- function checkRequiredState(def, issues) {
388
- for (const { entries, scope, path, label } of stateScopes(def))
450
+ function checkRequiredField(def, issues) {
451
+ for (const { entries, scope, path, label } of fieldScopes(def))
389
452
  for (const [n, entry] of (entries ?? []).entries())
390
453
  entry.required === !0 && (scope !== "workflow" ? issues.push({
391
454
  path: [...path, n, "required"],
392
455
  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`
393
456
  }) : entry.source.type !== "init" && issues.push({
394
457
  path: [...path, n, "required"],
395
- 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)`
458
+ 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)`
396
459
  }));
397
460
  }
398
- function checkStateEntryNames(def, issues) {
399
- for (const { entries, path, label } of stateScopes(def))
461
+ function checkFieldEntryNames(def, issues) {
462
+ for (const { entries, path, label } of fieldScopes(def))
400
463
  checkDuplicates(
401
464
  (entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
402
- `state entry name in ${label}`,
465
+ `field entry name in ${label}`,
403
466
  issues
404
467
  );
405
468
  }
@@ -499,15 +562,15 @@ function collectBindingSites(effects, path, label, sites) {
499
562
  });
500
563
  }
501
564
  function checkAssigneesEntries(def, issues) {
502
- for (const { entries, path } of stateScopes(def))
565
+ for (const { entries, path } of fieldScopes(def))
503
566
  (entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
504
567
  path,
505
- message: "at most one assignees-kind state entry per scope \u2014 the inbox reads it by kind"
568
+ message: "at most one assignees-kind field entry per scope \u2014 the inbox reads it by kind"
506
569
  });
507
570
  }
508
571
  function checkWorkflowInvariants(def) {
509
572
  const issues = [], stageNames = checkStages(def, issues);
510
- 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;
573
+ 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;
511
574
  }
512
575
  function defineWorkflow(definition) {
513
576
  const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(schema.AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
@@ -526,8 +589,8 @@ function defineAction(action) {
526
589
  function defineTransition(transition) {
527
590
  return parseOrThrow(schema.AuthoringTransitionSchema, transition, transitionLabel(transition));
528
591
  }
529
- function defineState(entry) {
530
- return parseOrThrow(schema.AuthoringStateEntrySchema, entry, labelFor("defineState", entry));
592
+ function defineField(entry) {
593
+ return parseOrThrow(schema.AuthoringFieldEntrySchema, entry, labelFor("defineField", entry));
531
594
  }
532
595
  function defineOp(op) {
533
596
  return parseOrThrow(schema.AuthoringOpSchema, op, "defineOp");
@@ -562,10 +625,10 @@ function labelFor(fn, value) {
562
625
  exports.defineAction = defineAction;
563
626
  exports.defineEffect = defineEffect;
564
627
  exports.defineEffectDescriptor = defineEffectDescriptor;
628
+ exports.defineField = defineField;
565
629
  exports.defineGuard = defineGuard;
566
630
  exports.defineOp = defineOp;
567
631
  exports.defineStage = defineStage;
568
- exports.defineState = defineState;
569
632
  exports.defineTask = defineTask;
570
633
  exports.defineTransition = defineTransition;
571
634
  exports.defineWorkflow = defineWorkflow;