@sanity/workflow-engine 0.9.0 → 0.11.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 { andConditions, expandRequiredRoles, 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
  }
@@ -14,13 +14,13 @@ function serializeGroqValue(value) {
14
14
  function desugarWorkflow(authoring) {
15
15
  const ctx = {
16
16
  issues: [],
17
- claimStates: /* @__PURE__ */ new Map(),
18
- claimedStates: /* @__PURE__ */ new Set(),
17
+ claimFields: /* @__PURE__ */ new Map(),
18
+ claimedFields: /* @__PURE__ */ new Set(),
19
19
  roleAliases: authoring.roleAliases
20
- }, workflowState = desugarStateEntries(authoring.state, ["state"], ctx), workflowLayer = layerOf(workflowState), stages = authoring.stages.map((stage, i) => {
21
- const path = ["stages", i], stageState = desugarStateEntries(stage.state, [...path, "state"], ctx), stageEnv = {
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 = {
22
22
  layers: [
23
- { scope: "stage", entries: layerOf(stageState) },
23
+ { scope: "stage", entries: layerOf(stageFields) },
24
24
  { scope: "workflow", entries: workflowLayer }
25
25
  ]
26
26
  }, tasks = (stage.tasks ?? []).map(
@@ -29,7 +29,7 @@ function desugarWorkflow(authoring) {
29
29
  (transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
30
30
  ), editable = desugarStageEditable(
31
31
  stage.editable,
32
- editableSlotNames(workflowState, stageState, tasks),
32
+ editableSlotNames(workflowFields, stageFields, tasks),
33
33
  [...path, "editable"],
34
34
  ctx
35
35
  );
@@ -40,13 +40,13 @@ function desugarWorkflow(authoring) {
40
40
  description: stage.description,
41
41
  guards: stage.guards
42
42
  }),
43
- ...stageState ? { state: stageState } : {},
43
+ ...stageFields ? { fields: stageFields } : {},
44
44
  ...tasks.length > 0 ? { tasks } : {},
45
45
  ...transitions.length > 0 ? { transitions } : {},
46
46
  ...editable ? { editable } : {}
47
47
  };
48
48
  });
49
- return checkUnclaimedClaimStates(ctx), { definition: {
49
+ return checkUnclaimedClaimFields(ctx), { definition: {
50
50
  ...stripUndefined({
51
51
  name: authoring.name,
52
52
  version: authoring.version,
@@ -57,11 +57,11 @@ function desugarWorkflow(authoring) {
57
57
  predicates: authoring.predicates,
58
58
  roleAliases: authoring.roleAliases
59
59
  }),
60
- ...workflowState ? { state: workflowState } : {},
60
+ ...workflowFields ? { fields: workflowFields } : {},
61
61
  stages
62
62
  }, issues: ctx.issues };
63
63
  }
64
- function desugarStateEntries(entries, path, ctx) {
64
+ function desugarFieldEntries(entries, path, ctx) {
65
65
  return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
66
66
  if (entry.type === "claim") {
67
67
  const desugared = {
@@ -69,7 +69,7 @@ function desugarStateEntries(entries, path, ctx) {
69
69
  type: "value.actor",
70
70
  source: { type: "write" }
71
71
  };
72
- return ctx.claimStates.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
72
+ return ctx.claimFields.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
73
73
  }
74
74
  const editable = normalizeEditable(entry.editable, [...path, i, "editable"], ctx);
75
75
  return {
@@ -100,12 +100,12 @@ function normalizeEditable(editable, path, ctx) {
100
100
  }
101
101
  return editable;
102
102
  }
103
- function editableSlotNames(workflowState, stageState, tasks) {
103
+ function editableSlotNames(workflowFields, stageFields, tasks) {
104
104
  const names = /* @__PURE__ */ new Set(), collect = (entries) => {
105
105
  for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
106
106
  };
107
- collect(workflowState), collect(stageState);
108
- for (const task of tasks) collect(task.state);
107
+ collect(workflowFields), collect(stageFields);
108
+ for (const task of tasks) collect(task.fields);
109
109
  return names;
110
110
  }
111
111
  function desugarStageEditable(overrides, inScope, path, ctx) {
@@ -128,16 +128,17 @@ function layerOf(entries) {
128
128
  return new Map((entries ?? []).map((entry) => [entry.name, entry]));
129
129
  }
130
130
  function desugarTask(task, path, stageEnv, ctx) {
131
- const taskState = desugarStateEntries(task.state, [...path, "state"], ctx), env = {
132
- 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]
133
133
  }, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
134
134
  (action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
135
- );
135
+ ), target = desugarTarget(task.target, env, [...path, "target"], ctx);
136
136
  return {
137
137
  ...stripUndefined({
138
138
  name: task.name,
139
139
  title: task.title,
140
140
  description: task.description,
141
+ kind: task.kind,
141
142
  filter: task.filter,
142
143
  requirements: task.requirements,
143
144
  completeWhen: task.completeWhen,
@@ -146,11 +147,21 @@ function desugarTask(task, path, stageEnv, ctx) {
146
147
  subworkflows: task.subworkflows
147
148
  }),
148
149
  activation: task.activation ?? "manual",
149
- ...taskState ? { state: taskState } : {},
150
+ ...target ? { target } : {},
151
+ ...taskFields ? { fields: taskFields } : {},
150
152
  ...ops ? { ops } : {},
151
153
  ...actions.length > 0 ? { actions } : {}
152
154
  };
153
155
  }
156
+ const TARGET_DOC_KINDS = ["doc.ref", "doc.refs", "release.ref"];
157
+ function desugarTarget(target, env, path, ctx) {
158
+ if (target === void 0 || target.type === "url") return target;
159
+ const ref = typeof target.field == "string" ? { field: target.field } : target.field, field = resolveRef(ref, env, [...path, "field"], ctx) ?? fallbackRef(ref), entry = entryAt(env, field);
160
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
161
+ path: [...path, "field"],
162
+ message: `manual task target references "${field.field}" of kind "${entry.type}" \u2014 a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
163
+ }), { type: "field", field };
164
+ }
154
165
  function desugarAction(action, path, env, taskName, ctx) {
155
166
  if ("type" in action)
156
167
  return desugarClaimAction(action, path, env, ctx);
@@ -168,19 +179,19 @@ function desugarAction(action, path, env, taskName, ctx) {
168
179
  };
169
180
  }
170
181
  function desugarClaimAction(action, path, env, ctx) {
171
- const ref = typeof action.state == "string" ? { state: action.state } : action.state, resolved = resolveRef(ref, env, [...path, "state"], ctx);
182
+ const ref = typeof action.field == "string" ? { field: action.field } : action.field, resolved = resolveRef(ref, env, [...path, "field"], ctx);
172
183
  if (resolved) {
173
184
  const entry = entryAt(env, resolved);
174
185
  entry && entry.type !== "value.actor" && ctx.issues.push({
175
- path: [...path, "state"],
176
- 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)`
177
- }), checkShadowedClaimTarget(action.name, ref.state, resolved, env, [...path, "state"], ctx), entry && ctx.claimedStates.add(entry);
186
+ path: [...path, "field"],
187
+ 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)`
188
+ }), checkShadowedClaimTarget(action.name, ref.field, resolved, env, [...path, "field"], ctx), entry && ctx.claimedFields.add(entry);
178
189
  }
179
- const noSteal = `!defined($state.${ref.state})`, filter = andConditions([
190
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([
180
191
  noSteal,
181
192
  rolesCondition(action.roles, ctx.roleAliases),
182
193
  action.filter
183
- ]), ops = resolved ? [{ type: "state.set", target: resolved, value: { type: "actor" } }] : [];
194
+ ]), ops = resolved ? [{ type: "field.set", target: resolved, value: { type: "actor" } }] : [];
184
195
  return {
185
196
  ...stripUndefined({
186
197
  name: action.name,
@@ -193,18 +204,18 @@ function desugarClaimAction(action, path, env, ctx) {
193
204
  ...ops.length > 0 ? { ops } : {}
194
205
  };
195
206
  }
196
- function checkShadowedClaimTarget(actionName, state, resolved, env, path, ctx) {
197
- const nearest = env.layers.find((l) => l.entries.has(state));
207
+ function checkShadowedClaimTarget(actionName, field, resolved, env, path, ctx) {
208
+ const nearest = env.layers.find((l) => l.entries.has(field));
198
209
  nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
199
210
  path,
200
- 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`
211
+ 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`
201
212
  });
202
213
  }
203
- function checkUnclaimedClaimStates(ctx) {
204
- for (const [entry, { name, path }] of ctx.claimStates)
205
- ctx.claimedStates.has(entry) || ctx.issues.push({
214
+ function checkUnclaimedClaimFields(ctx) {
215
+ for (const [entry, { name, path }] of ctx.claimFields)
216
+ ctx.claimedFields.has(entry) || ctx.issues.push({
206
217
  path,
207
- 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`
218
+ 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`
208
219
  });
209
220
  }
210
221
  function desugarTransition(transition, path, stageEnv, ctx) {
@@ -244,7 +255,7 @@ function desugarAuditOp(op, path, env, ctx) {
244
255
  return ctx.issues.push({
245
256
  path: [...path, "value"],
246
257
  message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
247
- }), { type: "state.append", target, value: op.value };
258
+ }), { type: "field.append", target, value: op.value };
248
259
  const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
249
260
  for (const [stamp, source] of [
250
261
  [actorField, AUDIT_STAMPS.actor],
@@ -259,23 +270,23 @@ function desugarAuditOp(op, path, env, ctx) {
259
270
  }
260
271
  fields[stamp] = source;
261
272
  }
262
- return { type: "state.append", target, value: { type: "object", fields } };
273
+ return { type: "field.append", target, value: { type: "object", fields } };
263
274
  }
264
275
  function resolveRef(ref, env, path, ctx) {
265
276
  const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
266
277
  for (const layer of layers)
267
- if (layer.entries.has(ref.state)) return { scope: layer.scope, state: ref.state };
278
+ if (layer.entries.has(ref.field)) return { scope: layer.scope, field: ref.field };
268
279
  const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
269
280
  ctx.issues.push({
270
281
  path,
271
- message: `state reference "${ref.state}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
282
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
272
283
  });
273
284
  }
274
285
  function entryAt(env, ref) {
275
- return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.state);
286
+ return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.field);
276
287
  }
277
288
  function fallbackRef(ref) {
278
- return { scope: ref.scope ?? "workflow", state: ref.state };
289
+ return { scope: ref.scope ?? "workflow", field: ref.field };
279
290
  }
280
291
  function rolesCondition(roles, aliases) {
281
292
  if (!(!roles || roles.length === 0))
@@ -288,7 +299,7 @@ function stripUndefined(obj) {
288
299
  return out;
289
300
  }
290
301
  const RESERVED_CONDITION_VARS = [
291
- "state",
302
+ "fields",
292
303
  "actor",
293
304
  "assigned",
294
305
  "can",
@@ -409,43 +420,43 @@ function checkGuardNames(def, issues) {
409
420
  issues
410
421
  );
411
422
  }
412
- function stateScopes(def) {
423
+ function fieldScopes(def) {
413
424
  const scopes = [
414
- { entries: def.state, scope: "workflow", path: ["state"], label: "workflow state" }
425
+ { entries: def.fields, scope: "workflow", path: ["fields"], label: "workflow fields" }
415
426
  ];
416
427
  for (const [i, stage] of def.stages.entries()) {
417
428
  scopes.push({
418
- entries: stage.state,
429
+ entries: stage.fields,
419
430
  scope: "stage",
420
- path: ["stages", i, "state"],
421
- label: `stage "${stage.name}" state`
431
+ path: ["stages", i, "fields"],
432
+ label: `stage "${stage.name}" fields`
422
433
  });
423
434
  for (const [j, task] of (stage.tasks ?? []).entries())
424
435
  scopes.push({
425
- entries: task.state,
436
+ entries: task.fields,
426
437
  scope: "task",
427
- path: ["stages", i, "tasks", j, "state"],
428
- label: `task "${task.name}" state`
438
+ path: ["stages", i, "tasks", j, "fields"],
439
+ label: `task "${task.name}" fields`
429
440
  });
430
441
  }
431
442
  return scopes;
432
443
  }
433
- function checkRequiredState(def, issues) {
434
- for (const { entries, scope, path, label } of stateScopes(def))
444
+ function checkRequiredField(def, issues) {
445
+ for (const { entries, scope, path, label } of fieldScopes(def))
435
446
  for (const [n, entry] of (entries ?? []).entries())
436
447
  entry.required === !0 && (scope !== "workflow" ? issues.push({
437
448
  path: [...path, n, "required"],
438
449
  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`
439
450
  }) : entry.source.type !== "init" && issues.push({
440
451
  path: [...path, n, "required"],
441
- 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)`
452
+ 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)`
442
453
  }));
443
454
  }
444
- function checkStateEntryNames(def, issues) {
445
- for (const { entries, path, label } of stateScopes(def))
455
+ function checkFieldEntryNames(def, issues) {
456
+ for (const { entries, path, label } of fieldScopes(def))
446
457
  checkDuplicates(
447
458
  (entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
448
- `state entry name in ${label}`,
459
+ `field entry name in ${label}`,
449
460
  issues
450
461
  );
451
462
  }
@@ -545,15 +556,55 @@ function collectBindingSites(effects, path, label, sites) {
545
556
  });
546
557
  }
547
558
  function checkAssigneesEntries(def, issues) {
548
- for (const { entries, path } of stateScopes(def))
559
+ for (const { entries, path } of fieldScopes(def))
549
560
  (entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
550
561
  path,
551
- message: "at most one assignees-kind state entry per scope \u2014 the inbox reads it by kind"
562
+ message: "at most one assignees-kind field entry per scope \u2014 the inbox reads it by kind"
552
563
  });
553
564
  }
565
+ function taskShape(task) {
566
+ return {
567
+ hasActions: (task.actions ?? []).length > 0,
568
+ hasEffects: (task.effects ?? []).length > 0,
569
+ hasCompleteWhen: task.completeWhen !== void 0,
570
+ hasSubworkflows: task.subworkflows !== void 0
571
+ };
572
+ }
573
+ const KIND_SHAPE_RULES = {
574
+ user: (s) => s.hasActions ? [] : ["needs at least one action for a person to fire"],
575
+ service: (s) => s.hasEffects ? [] : ["needs at least one effect \u2014 the automated work it performs"],
576
+ manual: (s) => [
577
+ s.hasActions || s.hasCompleteWhen ? void 0 : "needs a way to complete \u2014 an action to acknowledge it, or a `completeWhen` predicate to verify it (otherwise it auto-resolves on activation)",
578
+ s.hasSubworkflows ? "is off-system work, not a fan-out \u2014 drop `subworkflows` or move it to its own task" : void 0
579
+ ].filter((m) => m !== void 0),
580
+ receive: (s) => [
581
+ s.hasCompleteWhen || s.hasSubworkflows ? void 0 : "needs a `completeWhen` (or `subworkflows`) condition to wait on",
582
+ s.hasActions ? "has no actor, so it cannot carry actions" : void 0
583
+ ].filter((m) => m !== void 0),
584
+ script: (s) => [
585
+ s.hasActions ? "runs inline with no actor, so it cannot carry actions" : void 0,
586
+ s.hasCompleteWhen ? "resolves on activation, so it cannot carry a `completeWhen` (that is a `receive`/`service` wait)" : void 0,
587
+ s.hasSubworkflows ? "resolves on activation, so it cannot fan out with `subworkflows`" : void 0
588
+ ].filter((m) => m !== void 0)
589
+ };
590
+ function checkTaskKinds(def, issues) {
591
+ for (const [i, stage] of def.stages.entries())
592
+ for (const [j, task] of (stage.tasks ?? []).entries()) {
593
+ const path = ["stages", i, "tasks", j];
594
+ if (task.target !== void 0 && task.kind !== "manual" && issues.push({
595
+ path: [...path, "target"],
596
+ message: `task "${task.name}" has a \`target\` but is not \`kind: "manual"\` \u2014 a target is the deep-link for off-system manual work`
597
+ }), task.kind !== void 0)
598
+ for (const fault of KIND_SHAPE_RULES[task.kind](taskShape(task)))
599
+ issues.push({
600
+ path: [...path, "kind"],
601
+ message: `task "${task.name}" declares kind "${task.kind}" but ${fault}`
602
+ });
603
+ }
604
+ }
554
605
  function checkWorkflowInvariants(def) {
555
606
  const issues = [], stageNames = checkStages(def, issues);
556
- 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;
607
+ 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), checkTaskKinds(def, issues), issues;
557
608
  }
558
609
  function defineWorkflow(definition) {
559
610
  const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
@@ -572,8 +623,8 @@ function defineAction(action) {
572
623
  function defineTransition(transition) {
573
624
  return parseOrThrow(AuthoringTransitionSchema, transition, transitionLabel(transition));
574
625
  }
575
- function defineState(entry) {
576
- return parseOrThrow(AuthoringStateEntrySchema, entry, labelFor("defineState", entry));
626
+ function defineField(entry) {
627
+ return parseOrThrow(AuthoringFieldEntrySchema, entry, labelFor("defineField", entry));
577
628
  }
578
629
  function defineOp(op) {
579
630
  return parseOrThrow(AuthoringOpSchema, op, "defineOp");
@@ -609,10 +660,10 @@ export {
609
660
  defineAction,
610
661
  defineEffect,
611
662
  defineEffectDescriptor,
663
+ defineField,
612
664
  defineGuard,
613
665
  defineOp,
614
666
  defineStage,
615
- defineState,
616
667
  defineTask,
617
668
  defineTransition,
618
669
  defineWorkflow,