@sanity/workflow-engine 0.13.0 → 0.15.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/CHANGELOG.md +743 -0
- package/DATAMODEL.md +183 -0
- package/README.md +30 -0
- package/dist/_chunks-cjs/invariants.cjs +2461 -0
- package/dist/_chunks-es/invariants.js +2251 -0
- package/dist/define.cjs +676 -868
- package/dist/define.d.cts +1047 -85
- package/dist/define.d.ts +1047 -85
- package/dist/define.js +652 -865
- package/dist/index.cjs +9777 -5302
- package/dist/index.d.cts +3985 -733
- package/dist/index.d.ts +3985 -733
- package/dist/index.js +9401 -5359
- package/package.json +9 -5
- package/dist/_chunks-cjs/schema.cjs +0 -641
- package/dist/_chunks-cjs/schema.cjs.map +0 -1
- package/dist/_chunks-es/schema.js +0 -626
- package/dist/_chunks-es/schema.js.map +0 -1
- package/dist/define.cjs.map +0 -1
- package/dist/define.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/DATAMODEL.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# The engine's persisted data model
|
|
2
|
+
|
|
3
|
+
The engine owns three standalone document types in the Content Lake:
|
|
4
|
+
|
|
5
|
+
| Doc type | Constructed by | Mutated after create |
|
|
6
|
+
| ---------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `sanity.workflow.definition` | `planDefinitionDeploy` (`src/api/deploy.ts`) | never — deploys are create-only |
|
|
8
|
+
| `sanity.workflow.instance` | `buildInstanceBase` (`src/instance.ts`) | every persist writes the canonical state via `instanceStateFields` (`src/engine/mutation.ts`) |
|
|
9
|
+
| `temp.system.guard` | `compileGuard` (`src/core/guards.ts`) | full-body refresh on stage re-entry; predicate lift on retract |
|
|
10
|
+
|
|
11
|
+
These field trees are **our contract with every past and future engine
|
|
12
|
+
version reading the same dataset**. This file is where changes to them are
|
|
13
|
+
declared; the model-surface gates keep undeclared drift red:
|
|
14
|
+
|
|
15
|
+
- **The shape ledger** — `src/__tests__/model-shapes/model-<N>.json`, one
|
|
16
|
+
file per `DATA_MODEL_VERSION`, **append-only**: only the current version's
|
|
17
|
+
file is ever regenerated (`vitest -u` on the gate test), so past model
|
|
18
|
+
shapes stay checked in verbatim — the historical corpus future upgrade
|
|
19
|
+
machinery tests against. It pins the field trees of every doc
|
|
20
|
+
type built through the real constructors, plus the stored grammar's
|
|
21
|
+
vocabulary (every enum's values, every variant discriminator), so a value
|
|
22
|
+
rename is as red as a key rename.
|
|
23
|
+
- **Coverage tests** (`src/__tests__/model-surface.test.ts`) walk the stored
|
|
24
|
+
definition schema and fail when any declared key, discriminator, or
|
|
25
|
+
instantiable enum value stops appearing in the canonical document — the
|
|
26
|
+
grammar cannot grow past the fixture silently.
|
|
27
|
+
- **The lifecycle ledger** (`model-shapes/model-<N>.lifecycle.json` in
|
|
28
|
+
`workflow-engine-test`) drives real instances through the verbs —
|
|
29
|
+
including the recovery paths (expired-lease sweep, loop-back re-entry
|
|
30
|
+
adoption, dead-end propagation, abort) — and pins the interior trees a
|
|
31
|
+
fresh construction can't reach: `StageEntry` / `ActivityEntry` /
|
|
32
|
+
`SubworkflowEntry` rows, pending effects (claim included), effect
|
|
33
|
+
history, the idempotency ledger, and ALL `HistoryEntry` variants (the
|
|
34
|
+
accounting is compile-checked against the union, so a new variant fails
|
|
35
|
+
the build until the scenarios produce it).
|
|
36
|
+
|
|
37
|
+
Residual review responsibility: each ledger entry pins one shape per
|
|
38
|
+
variant (the scenario's first), so an _optional_ key that only appears on
|
|
39
|
+
some entries of an already-covered variant — or a conditionally-written
|
|
40
|
+
field no fixture or scenario reaches — still needs a reviewer to demand a
|
|
41
|
+
ledger-visible sample. Fixture width is part of reviewing a model change.
|
|
42
|
+
|
|
43
|
+
This is **not** definition-content versioning: `version` / `pinnedVersion` /
|
|
44
|
+
`contentHash` track what an author deployed; the model stamp tracks the shape
|
|
45
|
+
of the documents the engine itself writes.
|
|
46
|
+
|
|
47
|
+
## The stamp pair
|
|
48
|
+
|
|
49
|
+
The two doc types whose format the engine owns outright — definition and
|
|
50
|
+
instance — carry two numbers (`MODEL_STAMP` in `src/core/model-version.ts`).
|
|
51
|
+
Provenance and compatibility are deliberately separate, because consumers
|
|
52
|
+
sharing a dataset upgrade months apart (Functions lag Studios lag CLIs) and
|
|
53
|
+
"newer" must not mean "incompatible".
|
|
54
|
+
|
|
55
|
+
**The guard doc carries NO stamp.** `temp.system.guard` is a foreign
|
|
56
|
+
contract: the persisted format mirrors the Content Lake's forthcoming guard
|
|
57
|
+
primitive, and the engine must not grow fields on a shape it doesn't own —
|
|
58
|
+
the lake dictates that format long-term. The guard doc's versioning event is
|
|
59
|
+
the `_type` cutover itself (an engine that predates `system.guard` simply
|
|
60
|
+
never matches the new type).
|
|
61
|
+
|
|
62
|
+
- **`modelVersion` — provenance ("conforms-to").** The value of
|
|
63
|
+
`DATA_MODEL_VERSION` at write time: "this document conforms to model N
|
|
64
|
+
now". Bumps on **every** declared shape change, additive included.
|
|
65
|
+
Instances are re-stamped on every full persist (and rollback restore).
|
|
66
|
+
Partial patches deliberately leave the pair alone — they don't normalize
|
|
67
|
+
the shape, so restamping there would lie.
|
|
68
|
+
- **`minReaderModel` — the reader floor.** The value of
|
|
69
|
+
`DATA_MODEL_MIN_READER` at write time: the oldest engine model that can
|
|
70
|
+
safely _interpret_ this document. An **additive** change leaves the floor
|
|
71
|
+
untouched — older engines keep reading and writing newer docs, so
|
|
72
|
+
mixed-version fleets interoperate. Only a **breaking** change raises it,
|
|
73
|
+
and raising it is a declared decision to fence out older readers (fleet
|
|
74
|
+
must upgrade readers before such writers deploy).
|
|
75
|
+
- **Missing stamp = model 0** (floor 0) — the document was last written
|
|
76
|
+
before the stamp existed. A `modelVersion` with no `minReaderModel` is
|
|
77
|
+
malformed foreign data and gates conservatively: the stamp is the floor.
|
|
78
|
+
- **Backward compatibility is unconditional.** An engine at model N reads
|
|
79
|
+
every stamp `0..N`. There is no supported-version floor for _old_ docs;
|
|
80
|
+
introducing one is a deliberate future mechanism, never a side effect of
|
|
81
|
+
a shape change.
|
|
82
|
+
- **Fail hard on `minReaderModel` ahead — and only that.** Every engine read
|
|
83
|
+
of a stamped doc type (definition, instance) passes through
|
|
84
|
+
`assertReadableModel` — point reads, list queries, the drained-children
|
|
85
|
+
projection, snapshot hydration's raw reads. A floor ahead of the engine's
|
|
86
|
+
`DATA_MODEL_VERSION` throws
|
|
87
|
+
`ModelVersionAheadError` ("upgrade `@sanity/workflow-engine`") instead of
|
|
88
|
+
silently misreading a shape this engine predates. Deliberately wholesale:
|
|
89
|
+
one such document in a read's scope fails the whole read (a listing, a
|
|
90
|
+
verdict computation) rather than degrading silently. Two structural
|
|
91
|
+
exceptions: existence-only probes (no shape is interpreted), and lake-side
|
|
92
|
+
GROQ filters, which evaluate persisted fields server-side before any
|
|
93
|
+
client gate can run — their safety rests entirely on rule 3's
|
|
94
|
+
additive-only constraint.
|
|
95
|
+
- **Old writers restamp down — the round-trip rule.** An older engine
|
|
96
|
+
persisting a newer-but-compatible instance stamps its own `modelVersion`
|
|
97
|
+
(honest conforms-to; the stamp oscillates in mixed fleets, harmlessly).
|
|
98
|
+
This is safe because full persists are set-only patches and mutation
|
|
99
|
+
copies are spreads, so fields the older engine doesn't know **survive the
|
|
100
|
+
round-trip**. That is a load-bearing invariant: an additive change whose
|
|
101
|
+
fields would NOT survive an old writer's round-trip is not additive — it
|
|
102
|
+
is breaking, and must raise the floor.
|
|
103
|
+
- **The floor is raise-only.** A rewrite writes
|
|
104
|
+
`max(stored minReaderModel, own DATA_MODEL_MIN_READER)` — it never lowers
|
|
105
|
+
or drops the stored floor, because the preserved unknown fields of a newer
|
|
106
|
+
model may still require it. (The failure mode this fences off is
|
|
107
|
+
documented across the industry: a rewriter that drops the compatibility
|
|
108
|
+
gate it doesn't understand silently reopens the door the floor closed.)
|
|
109
|
+
- **Raising the floor is an availability decision, sequenced readers-first.**
|
|
110
|
+
Before any writer that raises `DATA_MODEL_MIN_READER` deploys, every
|
|
111
|
+
reader in the fleet — customer-deployed Functions foremost — must already
|
|
112
|
+
run an engine at or above the new floor (the BACKWARD deployment order:
|
|
113
|
+
upgrade consumers before producers). And like every engine-side check the
|
|
114
|
+
floor is advisory — it gates our own clients; the Content Lake stays the
|
|
115
|
+
sole enforcement point.
|
|
116
|
+
- **The gate ships from the engine.** Consumer surfaces that read these docs
|
|
117
|
+
without routing through the engine (the CLI's direct reads, MCP query
|
|
118
|
+
projections, the reactive studio/sdk adapters) adopt `assertReadableModel`
|
|
119
|
+
with the parse-don't-cast follow-up; until then they render newer-model
|
|
120
|
+
docs unchecked.
|
|
121
|
+
|
|
122
|
+
## Rules for changing the model
|
|
123
|
+
|
|
124
|
+
1. **Any change to a persisted field tree is a model change** and must be
|
|
125
|
+
declared: update the model-surface snapshot, add an entry to the model
|
|
126
|
+
log below, and decide — in the same change — whether `DATA_MODEL_VERSION`
|
|
127
|
+
moves **and whether the reader floor moves with it**. Nothing lands
|
|
128
|
+
"incidentally".
|
|
129
|
+
2. **Additive optional fields are allowed** without machinery: bump
|
|
130
|
+
`DATA_MODEL_VERSION`, leave `DATA_MODEL_MIN_READER` untouched, and
|
|
131
|
+
declare the tolerant read (what an absent value means on docs written
|
|
132
|
+
before the field existed) plus why the field survives an old writer's
|
|
133
|
+
round-trip.
|
|
134
|
+
3. **Renames, removals, and semantic changes are not allowed yet.** They
|
|
135
|
+
require versioned upgrade machinery (per-model normalization steps applied
|
|
136
|
+
on read) that does not exist; until it does, the model may only grow
|
|
137
|
+
additively. When such a change eventually lands, it raises
|
|
138
|
+
`DATA_MODEL_MIN_READER` — a declared fence against older readers, shipped
|
|
139
|
+
only after the fleet story is written.
|
|
140
|
+
4. **`DATA_MODEL_VERSION` moves when compatibility reasoning changes** — when
|
|
141
|
+
a reader would need to know which shape it is looking at. A purely
|
|
142
|
+
internal refactor that provably writes an identical tree does not bump.
|
|
143
|
+
5. **Classify by misinterpretation, not parseability.** The snapshot gate
|
|
144
|
+
catches structural drift only. A change can be structurally additive yet
|
|
145
|
+
change the MEANING of existing fields — old readers parse it fine and
|
|
146
|
+
silently misinterpret. That judgment (would an old reader misread, not
|
|
147
|
+
just fail to parse?) is exactly what decides whether
|
|
148
|
+
`DATA_MODEL_MIN_READER` moves, and no snapshot can make it — each model
|
|
149
|
+
log entry states it explicitly.
|
|
150
|
+
|
|
151
|
+
## Model log
|
|
152
|
+
|
|
153
|
+
### Model 0 — pre-governance
|
|
154
|
+
|
|
155
|
+
Everything written before the stamp existed. Documents carry no
|
|
156
|
+
`modelVersion`. The shape history of model 0 was absorbed by tolerant reads
|
|
157
|
+
that predate this file — recorded here so they are declared compatibility
|
|
158
|
+
rules, not folklore:
|
|
159
|
+
|
|
160
|
+
- **Instances without `subworkflows[]`** (persisted before the subworkflow
|
|
161
|
+
registry existed) — read as `[]`; the next full persist materializes it.
|
|
162
|
+
- **Instances without `processedRequests[]`** (persisted before the
|
|
163
|
+
idempotency ledger existed) — read as `[]`; the next full persist
|
|
164
|
+
materializes it.
|
|
165
|
+
- **Definitions without `contentHash`** (deployed before content-addressing)
|
|
166
|
+
— always read as changed, so the first redeploy after upgrade mints a
|
|
167
|
+
fresh version.
|
|
168
|
+
|
|
169
|
+
### Model 1 — stamping introduced (reader floor: 0)
|
|
170
|
+
|
|
171
|
+
The `modelVersion` + `minReaderModel` pair is stamped on the definition and
|
|
172
|
+
instance doc types; the guard doc deliberately carries none (foreign lake
|
|
173
|
+
contract — see "The stamp pair"). No other shape change. The floor is 0: any
|
|
174
|
+
engine ever released reads model-1 documents fine, and model-0 documents
|
|
175
|
+
keep reading exactly as before.
|
|
176
|
+
|
|
177
|
+
## Pending governed changes
|
|
178
|
+
|
|
179
|
+
- **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`
|
|
180
|
+
prefix drops when the lake's own guard primitive ships (see
|
|
181
|
+
`GUARD_DOC_TYPE` in `src/types/authorization.ts`). A breaking type change
|
|
182
|
+
for already-persisted guard docs; it must land as a declared model change
|
|
183
|
+
with a migration story, through this process.
|
package/README.md
CHANGED
|
@@ -13,6 +13,36 @@ effects for runtimes to drain.
|
|
|
13
13
|
npm install @sanity/workflow-engine
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Fields vs Sanity schema fields
|
|
17
|
+
|
|
18
|
+
A workflow **field** is the engine's state primitive: a typed value a workflow
|
|
19
|
+
instance carries — an assignee, a decision, a due date, a reference to the
|
|
20
|
+
document under review. Fields are declared in `fields: [...]` at workflow,
|
|
21
|
+
stage, or activity scope and live on the workflow **instance** document. They
|
|
22
|
+
are not Sanity schema fields: they never touch your content documents' shape.
|
|
23
|
+
|
|
24
|
+
The authoring DSL deliberately borrows Sanity's schema idiom, so
|
|
25
|
+
`@sanity/workflow-engine/define` exports its own `defineField`. A Studio file
|
|
26
|
+
that also imports `defineField` from `sanity` should alias one of them:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import {defineField as defineWorkflowField} from '@sanity/workflow-engine/define'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `fields` key itself appears at four nesting levels, meaning something
|
|
33
|
+
different at each — read it by where it sits:
|
|
34
|
+
|
|
35
|
+
1. **Declared field entries** — on a definition's workflow / stage / activity
|
|
36
|
+
nodes: the typed state that scope carries.
|
|
37
|
+
2. **Sub-field shapes** — inside an `object`- or `array`-kind field entry:
|
|
38
|
+
lightweight `{type, name}` shapes for the value's structure, mirroring
|
|
39
|
+
Sanity's `object.fields` / `array.of`.
|
|
40
|
+
3. **Object value expressions** — a `{type: 'object', fields: {...}}` value
|
|
41
|
+
expression in an op payload: one expression per key of the computed
|
|
42
|
+
object value.
|
|
43
|
+
4. **Resolved runtime values** — `fields` on a workflow instance document: the
|
|
44
|
+
declared entries with their current values, written by ops and edits.
|
|
45
|
+
|
|
16
46
|
## License
|
|
17
47
|
|
|
18
48
|
[MIT](./LICENSE)
|