@sanity/workflow-engine 0.18.0 → 0.19.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 +9 -0
- package/DATAMODEL.md +53 -0
- package/dist/_chunks-cjs/invariants.cjs +87 -17
- package/dist/_chunks-es/invariants.js +87 -17
- package/dist/define.d.cts +212 -4
- package/dist/define.d.ts +212 -4
- package/dist/index.cjs +2334 -1864
- package/dist/index.d.cts +455 -9
- package/dist/index.d.ts +455 -9
- package/dist/index.js +2313 -1855
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @sanity/workflow-engine
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 24b11dd: Effect handlers can report intermediate field state mid-dispatch. The handler context gains `ctx.commitOps({ops, idempotencyKey})` — field ops only, validated like completion ops, executed through the new `workflow.commitEffectOps` verb as a standard engine commit (gate, history, idempotency ledger, compare-and-swap persist, guard refresh, cascade) — and `ctx.setProgress(field, value)`, sugar that coalesces pending absolute sets per field. Calls enqueue synchronously into a bounded per-dispatch queue (depth 32, 200 commits per dispatch) that serializes commits in call order; overflow throws synchronously at the call site, and settlement drains the queue before completion so an un-awaited failed report still fails the effect. Pending-effect claims now carry a `claimToken` minted on every claim and takeover: a report from an expired, released, or superseded dispatch is rejected before writing (`StaleEffectClaimError`), and a successful report renews the claim's lease. Completion semantics are unchanged and remain claim-blind first-writer-wins. Each report emits the sampled `Editorial Workflows Effect State Reported` telemetry event (adoption signal, max one per minute). Persisted-model note: the claim token rides persisted model 3 additively (see `DATAMODEL.md`).
|
|
8
|
+
- 2005ab7: Action evaluations carry a `firing` consequence (`FiringConsequence` `{exitsStage, transition?}`): what firing the action would do to the flow right now, computed by replaying the fire in memory through the same machinery a real commit runs — ops, the triggered fixpoint, then transition selection — against the projection's state. The answer is total for the current state and conditional on the commit landing; it is omitted where the replay can't be faithful (caller params, a spawn's dataset-driven fan-out in the hop, a terminal instance, a stage visit whose entries have diverged from the pinned definition) and on cascade-fired actions. Advisory, like every derived verdict. Wrong-shape `field.updateWhere` aborts (a merge resolving to a non-object; a resolved merge carrying reserved row keys) now throw the typed `FieldValueShapeError` on the commit path, joining the other wrong-shape write aborts.
|
|
9
|
+
- 24b11dd: Add the `progress` field kind: an elevated `number` carrying application-defined 0–100 completion, so Studio and SDK surfaces can identify and elevate it without guessing which number field means "how far along". Every non-null value must be finite and within 0–100 inclusive (fractions allowed), enforced by one value schema at every write boundary — declaration seeds, start values, edits, ops, nested sub-fields, and effect outputs. Choice lists are rejected on the kind at authoring time. Mints persisted model 3 additively (see `DATAMODEL.md`).
|
|
10
|
+
- 59b40fd: **BREAKING:** a deployment's `name` is now its identity in `defineWorkflowConfig`: enforced unique across the config and constrained to the same grammar as `tag` (lowercase letters, digits, dashes — free text like "My cool workflow" is rejected). Tags may now repeat across deployments; the enforced storage invariant is instead the `(workflowResource, tag)` pair — two deployments sharing both are rejected with each entry named, since they would write into the same partition and fight over definition versions.
|
|
11
|
+
|
|
3
12
|
## 0.18.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/DATAMODEL.md
CHANGED
|
@@ -445,6 +445,59 @@ Spread-based mutation copies preserve `validation` across every full persist;
|
|
|
445
445
|
the derived floor prevents older writers from reaching constrained model-2
|
|
446
446
|
instances.
|
|
447
447
|
|
|
448
|
+
### Model 3 — the `progress` field kind + effect claim tokens (reader floor: 2)
|
|
449
|
+
|
|
450
|
+
Two additive growths ship together as model 3 — model 2 was released (npm
|
|
451
|
+
`@sanity/workflow-engine@0.18.0`) before either landed, so they mint the next
|
|
452
|
+
version rather than amending a shipped one. Neither raises the reader floor;
|
|
453
|
+
the writer maximum stays 2.
|
|
454
|
+
|
|
455
|
+
**The `progress` field kind.** An elevated `number` (same stored numeric
|
|
456
|
+
value) carrying application-defined 0–100 completion, so surfaces can
|
|
457
|
+
identify and elevate it without guessing which number field means "how far
|
|
458
|
+
along". Every non-null value is finite and within 0–100 inclusive (fractions
|
|
459
|
+
allowed), enforced at every write boundary by the kind's one value schema.
|
|
460
|
+
Model 2's `validation` tree composes with it as NARROWING only: each declared
|
|
461
|
+
bound must itself sit within 0–100, gated at authoring. Both stamped trees
|
|
462
|
+
grow the value:
|
|
463
|
+
|
|
464
|
+
- **Definition tree** — `fields[].type` (and nested `FieldShape.type`)
|
|
465
|
+
admits `'progress'`.
|
|
466
|
+
- **Instance tree** — resolved `fields[]` entries admit `_type: 'progress'`
|
|
467
|
+
(the same arm shape as `number`).
|
|
468
|
+
|
|
469
|
+
Manifest feature: `progress-field-kind` (definition + instance, additive,
|
|
470
|
+
detectable, floor 0).
|
|
471
|
+
|
|
472
|
+
Would an old reader misread the kind (rule 5)? No — the same loud refusal as
|
|
473
|
+
the `subject` kind: an older engine (model 2 and below) that loaded a
|
|
474
|
+
progress-carrying instance fails the strict `fields[]` variant with
|
|
475
|
+
`PersistedDocShapeError` before interpreting anything, and cannot deploy or
|
|
476
|
+
evaluate a progress-declaring definition. No older code path can write a
|
|
477
|
+
progress field while skipping its bound, so there is no silent bypass to
|
|
478
|
+
fence — the kind leaves the floor at 0.
|
|
479
|
+
|
|
480
|
+
**Effect claim tokens.** `pendingEffects[].claim` grows an optional
|
|
481
|
+
`claimToken` — an opaque identifier minted fresh on every claim and
|
|
482
|
+
takeover. It is the exact-claim identity a mid-dispatch state report
|
|
483
|
+
(`commitEffectOps`) must present: `effectKey` cannot serve because a lease
|
|
484
|
+
takeover keeps the same key. A successful report also renews the claim's
|
|
485
|
+
`leaseExpiresAt` in the same commit.
|
|
486
|
+
|
|
487
|
+
Manifest feature: `effect-claim-tokens` (instance, additive, detectable,
|
|
488
|
+
floor 0).
|
|
489
|
+
|
|
490
|
+
Tolerant read: an older engine's claim parse admits unknown keys, so the
|
|
491
|
+
token rides through untouched and means nothing to it — older claim
|
|
492
|
+
semantics (presence + lease) are unchanged. Round-trip survival: full
|
|
493
|
+
persists derive from the complete loaded post-state, so an older writer
|
|
494
|
+
carries the token along; the claim-REPLACING writes (takeover, sweep
|
|
495
|
+
release, completion drain) discard it deliberately, which is the token's
|
|
496
|
+
own lifecycle — a report presenting a discarded token is rejected before
|
|
497
|
+
writing, failing closed. A claim without a token (older writer's takeover)
|
|
498
|
+
simply cannot authorise mid-dispatch reports; dispatch and completion are
|
|
499
|
+
unaffected.
|
|
500
|
+
|
|
448
501
|
## Pending governed changes
|
|
449
502
|
|
|
450
503
|
- **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`
|
|
@@ -129,7 +129,7 @@ function parentRef(instance) {
|
|
|
129
129
|
return instance.ancestors.at(-1);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
const DATA_MODEL_VERSION =
|
|
132
|
+
const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
|
|
133
133
|
|
|
134
134
|
class ReaderModelAcknowledgementError extends WorkflowError {
|
|
135
135
|
code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
|
|
@@ -188,6 +188,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
|
|
|
188
188
|
compatibility: "reader-floor",
|
|
189
189
|
applicability: "detectable",
|
|
190
190
|
summary: "String, text, and number values may carry persisted inclusive bounds."
|
|
191
|
+
}), Object.freeze({
|
|
192
|
+
id: "progress-field-kind",
|
|
193
|
+
introducedInModel: 3,
|
|
194
|
+
minReaderModel: 0,
|
|
195
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
196
|
+
compatibility: "additive",
|
|
197
|
+
applicability: "detectable",
|
|
198
|
+
summary: "A progress field kind carries application-defined 0–100 completion."
|
|
199
|
+
}), Object.freeze({
|
|
200
|
+
id: "effect-claim-tokens",
|
|
201
|
+
introducedInModel: 3,
|
|
202
|
+
minReaderModel: 0,
|
|
203
|
+
documentTypes: Object.freeze([ "instance" ]),
|
|
204
|
+
compatibility: "additive",
|
|
205
|
+
applicability: "detectable",
|
|
206
|
+
summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
|
|
191
207
|
}) ]);
|
|
192
208
|
|
|
193
209
|
function recordOf(value) {
|
|
@@ -224,8 +240,8 @@ function hasChoiceList(document) {
|
|
|
224
240
|
});
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
function
|
|
228
|
-
return persistedFieldEntries(document).some(entry => entry.type ===
|
|
243
|
+
function hasFieldKind(document, kind) {
|
|
244
|
+
return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
function hasActionSemantics(document) {
|
|
@@ -240,12 +256,22 @@ function hasScalarValidation(document) {
|
|
|
240
256
|
});
|
|
241
257
|
}
|
|
242
258
|
|
|
259
|
+
function hasClaimTokens(document) {
|
|
260
|
+
const root = recordOf(document);
|
|
261
|
+
return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
|
|
262
|
+
const claim = recordOf(entry.claim);
|
|
263
|
+
return claim !== void 0 && typeof claim.claimToken == "string";
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
243
267
|
const featureDetectors = {
|
|
244
268
|
"governed-model-stamps": () => !0,
|
|
245
|
-
"subject-field-kind":
|
|
269
|
+
"subject-field-kind": document => hasFieldKind(document, "subject"),
|
|
246
270
|
"typed-scalar-choice-lists": hasChoiceList,
|
|
247
271
|
"action-semantics": hasActionSemantics,
|
|
248
|
-
"inclusive-scalar-bounds": hasScalarValidation
|
|
272
|
+
"inclusive-scalar-bounds": hasScalarValidation,
|
|
273
|
+
"progress-field-kind": document => hasFieldKind(document, "progress"),
|
|
274
|
+
"effect-claim-tokens": hasClaimTokens
|
|
249
275
|
};
|
|
250
276
|
|
|
251
277
|
function requiredModelFeatures(documentType, document) {
|
|
@@ -547,7 +573,13 @@ function asPredicate(validate) {
|
|
|
547
573
|
};
|
|
548
574
|
}
|
|
549
575
|
|
|
550
|
-
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts)
|
|
576
|
+
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
|
|
577
|
+
|
|
578
|
+
function lakeSegment(label) {
|
|
579
|
+
return v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty(), v__namespace.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
|
|
551
583
|
type: v__namespace.literal("dataset"),
|
|
552
584
|
id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
553
585
|
}), v__namespace.object({
|
|
@@ -563,14 +595,46 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
|
|
|
563
595
|
name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
564
596
|
resource: WorkflowResourceSchema
|
|
565
597
|
}), DefinitionSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v__namespace.object({
|
|
566
|
-
name:
|
|
598
|
+
name: lakeSegment("name"),
|
|
567
599
|
expectedMinReaderModel: v__namespace.optional(v__namespace.custom(() => !0), void 0),
|
|
568
|
-
tag:
|
|
600
|
+
tag: lakeSegment("tag"),
|
|
569
601
|
workflowResource: WorkflowResourceSchema,
|
|
570
|
-
resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.check(bindings =>
|
|
602
|
+
resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
|
|
571
603
|
definitions: v__namespace.pipe(v__namespace.array(DefinitionSchema), v__namespace.minLength(1, "a deployment needs at least one definition"))
|
|
572
|
-
})
|
|
573
|
-
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
function firstDuplicatePair(items, keyOf) {
|
|
607
|
+
const seen = /* @__PURE__ */ new Map;
|
|
608
|
+
for (const item of items) {
|
|
609
|
+
const key = keyOf(item), earlier = seen.get(key);
|
|
610
|
+
if (earlier !== void 0) return [ earlier, item ];
|
|
611
|
+
seen.set(key, item);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function duplicateHandleMessage(bindings) {
|
|
616
|
+
const pair = firstDuplicatePair(bindings, binding => binding.name);
|
|
617
|
+
if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function duplicateNameMessage(deployments) {
|
|
621
|
+
const pair = firstDuplicatePair(deployments, deployment => deployment.name);
|
|
622
|
+
if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function partitionKey(deployment) {
|
|
626
|
+
return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function partitionCollisionMessage(deployments) {
|
|
630
|
+
const pair = firstDuplicatePair(deployments, partitionKey);
|
|
631
|
+
if (pair === void 0) return;
|
|
632
|
+
const [first, second] = pair;
|
|
633
|
+
return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const TelemetryLoggerSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v__namespace.object({
|
|
637
|
+
deployments: v__namespace.pipe(v__namespace.array(DeploymentSchema), v__namespace.minLength(1, "a config needs at least one deployment"), v__namespace.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v__namespace.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
|
|
574
638
|
telemetry: v__namespace.optional(TelemetryLoggerSchema)
|
|
575
639
|
});
|
|
576
640
|
|
|
@@ -1724,7 +1788,7 @@ const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s
|
|
|
1724
1788
|
}), tolerantObject()({
|
|
1725
1789
|
type: v__namespace.literal("role"),
|
|
1726
1790
|
role: NonEmptyString
|
|
1727
|
-
}) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), IsoTimestamp ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
|
|
1791
|
+
}) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableProgress = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.number(), v__namespace.finite("progress must be a finite number"), v__namespace.minValue(0, "progress must be at least 0"), v__namespace.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v__namespace.union([ v__namespace.null(), IsoTimestamp ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
|
|
1728
1792
|
|
|
1729
1793
|
function normalizedChoiceKind(kind) {
|
|
1730
1794
|
return kind === "dateTime" ? "datetime" : kind;
|
|
@@ -1756,6 +1820,7 @@ const fieldValueSchemas = {
|
|
|
1756
1820
|
string: NullableString,
|
|
1757
1821
|
text: NullableString,
|
|
1758
1822
|
number: NullableNumber,
|
|
1823
|
+
progress: NullableProgress,
|
|
1759
1824
|
boolean: NullableBoolean,
|
|
1760
1825
|
date: NullableDate,
|
|
1761
1826
|
datetime: NullableDateTime,
|
|
@@ -1780,13 +1845,13 @@ function shapeValueSchema(shape, leaf) {
|
|
|
1780
1845
|
}
|
|
1781
1846
|
|
|
1782
1847
|
function scalarMeasurement(entryType, value) {
|
|
1783
|
-
if (entryType === "number" && typeof value == "number") return value;
|
|
1848
|
+
if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
|
|
1784
1849
|
if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
|
|
1785
1850
|
}
|
|
1786
1851
|
|
|
1787
1852
|
function scalarBoundIssue(args) {
|
|
1788
1853
|
const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
|
|
1789
|
-
return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
|
|
1854
|
+
return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
|
|
1790
1855
|
}
|
|
1791
1856
|
|
|
1792
1857
|
function scalarValidationIssues(args) {
|
|
@@ -2079,7 +2144,7 @@ function groupMembershipNames(group) {
|
|
|
2079
2144
|
return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
|
|
2080
2145
|
}
|
|
2081
2146
|
|
|
2082
|
-
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v__namespace.pipe(v__namespace.number(), v__namespace.finite("must be finite")), ScalarValidationSchema = v__namespace.pipe(v__namespace.strictObject({
|
|
2147
|
+
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v__namespace.pipe(v__namespace.number(), v__namespace.finite("must be finite")), ScalarValidationSchema = v__namespace.pipe(v__namespace.strictObject({
|
|
2083
2148
|
min: v__namespace.optional(FiniteNumber),
|
|
2084
2149
|
max: v__namespace.optional(FiniteNumber)
|
|
2085
2150
|
}), v__namespace.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v__namespace.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v__namespace.strictObject({
|
|
@@ -2188,7 +2253,7 @@ function choiceOptionsCheck() {
|
|
|
2188
2253
|
}) ?? []).join("; "));
|
|
2189
2254
|
}
|
|
2190
2255
|
|
|
2191
|
-
const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
|
|
2256
|
+
const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
|
|
2192
2257
|
|
|
2193
2258
|
function scalarValidationCheck() {
|
|
2194
2259
|
return v__namespace.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
|
|
@@ -2197,7 +2262,11 @@ function scalarValidationCheck() {
|
|
|
2197
2262
|
function scalarValidationDeclarationIssues(entry) {
|
|
2198
2263
|
const {type: type, validation: validation} = entry;
|
|
2199
2264
|
if (validation === void 0) return;
|
|
2200
|
-
if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
|
|
2265
|
+
if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
|
|
2266
|
+
if (type === "progress") {
|
|
2267
|
+
const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
|
|
2268
|
+
return issues2.length === 0 ? void 0 : issues2;
|
|
2269
|
+
}
|
|
2201
2270
|
if (type === "number") return;
|
|
2202
2271
|
const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
|
|
2203
2272
|
return issues.length === 0 ? void 0 : issues;
|
|
@@ -3608,6 +3677,7 @@ const SCALAR = {
|
|
|
3608
3677
|
string: () => SCALAR,
|
|
3609
3678
|
text: () => SCALAR,
|
|
3610
3679
|
number: () => SCALAR,
|
|
3680
|
+
progress: () => SCALAR,
|
|
3611
3681
|
boolean: () => SCALAR,
|
|
3612
3682
|
date: () => SCALAR,
|
|
3613
3683
|
datetime: () => SCALAR,
|
|
@@ -113,7 +113,7 @@ function parentRef(instance) {
|
|
|
113
113
|
return instance.ancestors.at(-1);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const DATA_MODEL_VERSION =
|
|
116
|
+
const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
|
|
117
117
|
|
|
118
118
|
class ReaderModelAcknowledgementError extends WorkflowError {
|
|
119
119
|
code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
|
|
@@ -172,6 +172,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
|
|
|
172
172
|
compatibility: "reader-floor",
|
|
173
173
|
applicability: "detectable",
|
|
174
174
|
summary: "String, text, and number values may carry persisted inclusive bounds."
|
|
175
|
+
}), Object.freeze({
|
|
176
|
+
id: "progress-field-kind",
|
|
177
|
+
introducedInModel: 3,
|
|
178
|
+
minReaderModel: 0,
|
|
179
|
+
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
180
|
+
compatibility: "additive",
|
|
181
|
+
applicability: "detectable",
|
|
182
|
+
summary: "A progress field kind carries application-defined 0–100 completion."
|
|
183
|
+
}), Object.freeze({
|
|
184
|
+
id: "effect-claim-tokens",
|
|
185
|
+
introducedInModel: 3,
|
|
186
|
+
minReaderModel: 0,
|
|
187
|
+
documentTypes: Object.freeze([ "instance" ]),
|
|
188
|
+
compatibility: "additive",
|
|
189
|
+
applicability: "detectable",
|
|
190
|
+
summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
|
|
175
191
|
}) ]);
|
|
176
192
|
|
|
177
193
|
function recordOf(value) {
|
|
@@ -208,8 +224,8 @@ function hasChoiceList(document) {
|
|
|
208
224
|
});
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
function
|
|
212
|
-
return persistedFieldEntries(document).some(entry => entry.type ===
|
|
227
|
+
function hasFieldKind(document, kind) {
|
|
228
|
+
return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
|
|
213
229
|
}
|
|
214
230
|
|
|
215
231
|
function hasActionSemantics(document) {
|
|
@@ -224,12 +240,22 @@ function hasScalarValidation(document) {
|
|
|
224
240
|
});
|
|
225
241
|
}
|
|
226
242
|
|
|
243
|
+
function hasClaimTokens(document) {
|
|
244
|
+
const root = recordOf(document);
|
|
245
|
+
return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
|
|
246
|
+
const claim = recordOf(entry.claim);
|
|
247
|
+
return claim !== void 0 && typeof claim.claimToken == "string";
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
227
251
|
const featureDetectors = {
|
|
228
252
|
"governed-model-stamps": () => !0,
|
|
229
|
-
"subject-field-kind":
|
|
253
|
+
"subject-field-kind": document => hasFieldKind(document, "subject"),
|
|
230
254
|
"typed-scalar-choice-lists": hasChoiceList,
|
|
231
255
|
"action-semantics": hasActionSemantics,
|
|
232
|
-
"inclusive-scalar-bounds": hasScalarValidation
|
|
256
|
+
"inclusive-scalar-bounds": hasScalarValidation,
|
|
257
|
+
"progress-field-kind": document => hasFieldKind(document, "progress"),
|
|
258
|
+
"effect-claim-tokens": hasClaimTokens
|
|
233
259
|
};
|
|
234
260
|
|
|
235
261
|
function requiredModelFeatures(documentType, document) {
|
|
@@ -531,7 +557,13 @@ function asPredicate(validate) {
|
|
|
531
557
|
};
|
|
532
558
|
}
|
|
533
559
|
|
|
534
|
-
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts)
|
|
560
|
+
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
|
|
561
|
+
|
|
562
|
+
function lakeSegment(label) {
|
|
563
|
+
return v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const WorkflowResourceSchema = v.variant("type", [ v.object({
|
|
535
567
|
type: v.literal("dataset"),
|
|
536
568
|
id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
537
569
|
}), v.object({
|
|
@@ -547,14 +579,46 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
|
|
|
547
579
|
name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
548
580
|
resource: WorkflowResourceSchema
|
|
549
581
|
}), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
|
|
550
|
-
name:
|
|
582
|
+
name: lakeSegment("name"),
|
|
551
583
|
expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
|
|
552
|
-
tag:
|
|
584
|
+
tag: lakeSegment("tag"),
|
|
553
585
|
workflowResource: WorkflowResourceSchema,
|
|
554
|
-
resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings =>
|
|
586
|
+
resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
|
|
555
587
|
definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition"))
|
|
556
|
-
})
|
|
557
|
-
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
function firstDuplicatePair(items, keyOf) {
|
|
591
|
+
const seen = /* @__PURE__ */ new Map;
|
|
592
|
+
for (const item of items) {
|
|
593
|
+
const key = keyOf(item), earlier = seen.get(key);
|
|
594
|
+
if (earlier !== void 0) return [ earlier, item ];
|
|
595
|
+
seen.set(key, item);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function duplicateHandleMessage(bindings) {
|
|
600
|
+
const pair = firstDuplicatePair(bindings, binding => binding.name);
|
|
601
|
+
if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function duplicateNameMessage(deployments) {
|
|
605
|
+
const pair = firstDuplicatePair(deployments, deployment => deployment.name);
|
|
606
|
+
if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function partitionKey(deployment) {
|
|
610
|
+
return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function partitionCollisionMessage(deployments) {
|
|
614
|
+
const pair = firstDuplicatePair(deployments, partitionKey);
|
|
615
|
+
if (pair === void 0) return;
|
|
616
|
+
const [first, second] = pair;
|
|
617
|
+
return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
|
|
621
|
+
deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
|
|
558
622
|
telemetry: v.optional(TelemetryLoggerSchema)
|
|
559
623
|
});
|
|
560
624
|
|
|
@@ -1708,7 +1772,7 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
|
|
|
1708
1772
|
}), tolerantObject()({
|
|
1709
1773
|
type: v.literal("role"),
|
|
1710
1774
|
role: NonEmptyString
|
|
1711
|
-
}) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
|
|
1775
|
+
}) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableProgress = v.union([ v.null(), v.pipe(v.number(), v.finite("progress must be a finite number"), v.minValue(0, "progress must be at least 0"), v.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
|
|
1712
1776
|
|
|
1713
1777
|
function normalizedChoiceKind(kind) {
|
|
1714
1778
|
return kind === "dateTime" ? "datetime" : kind;
|
|
@@ -1740,6 +1804,7 @@ const fieldValueSchemas = {
|
|
|
1740
1804
|
string: NullableString,
|
|
1741
1805
|
text: NullableString,
|
|
1742
1806
|
number: NullableNumber,
|
|
1807
|
+
progress: NullableProgress,
|
|
1743
1808
|
boolean: NullableBoolean,
|
|
1744
1809
|
date: NullableDate,
|
|
1745
1810
|
datetime: NullableDateTime,
|
|
@@ -1764,13 +1829,13 @@ function shapeValueSchema(shape, leaf) {
|
|
|
1764
1829
|
}
|
|
1765
1830
|
|
|
1766
1831
|
function scalarMeasurement(entryType, value) {
|
|
1767
|
-
if (entryType === "number" && typeof value == "number") return value;
|
|
1832
|
+
if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
|
|
1768
1833
|
if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
|
|
1769
1834
|
}
|
|
1770
1835
|
|
|
1771
1836
|
function scalarBoundIssue(args) {
|
|
1772
1837
|
const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
|
|
1773
|
-
return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
|
|
1838
|
+
return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
|
|
1774
1839
|
}
|
|
1775
1840
|
|
|
1776
1841
|
function scalarValidationIssues(args) {
|
|
@@ -2063,7 +2128,7 @@ function groupMembershipNames(group) {
|
|
|
2063
2128
|
return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
|
|
2064
2129
|
}
|
|
2065
2130
|
|
|
2066
|
-
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
|
|
2131
|
+
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
|
|
2067
2132
|
min: v.optional(FiniteNumber),
|
|
2068
2133
|
max: v.optional(FiniteNumber)
|
|
2069
2134
|
}), v.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v.strictObject({
|
|
@@ -2172,7 +2237,7 @@ function choiceOptionsCheck() {
|
|
|
2172
2237
|
}) ?? []).join("; "));
|
|
2173
2238
|
}
|
|
2174
2239
|
|
|
2175
|
-
const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
|
|
2240
|
+
const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
|
|
2176
2241
|
|
|
2177
2242
|
function scalarValidationCheck() {
|
|
2178
2243
|
return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
|
|
@@ -2181,7 +2246,11 @@ function scalarValidationCheck() {
|
|
|
2181
2246
|
function scalarValidationDeclarationIssues(entry) {
|
|
2182
2247
|
const {type: type, validation: validation} = entry;
|
|
2183
2248
|
if (validation === void 0) return;
|
|
2184
|
-
if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
|
|
2249
|
+
if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
|
|
2250
|
+
if (type === "progress") {
|
|
2251
|
+
const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
|
|
2252
|
+
return issues2.length === 0 ? void 0 : issues2;
|
|
2253
|
+
}
|
|
2185
2254
|
if (type === "number") return;
|
|
2186
2255
|
const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
|
|
2187
2256
|
return issues.length === 0 ? void 0 : issues;
|
|
@@ -3592,6 +3661,7 @@ const SCALAR = {
|
|
|
3592
3661
|
string: () => SCALAR,
|
|
3593
3662
|
text: () => SCALAR,
|
|
3594
3663
|
number: () => SCALAR,
|
|
3664
|
+
progress: () => SCALAR,
|
|
3595
3665
|
boolean: () => SCALAR,
|
|
3596
3666
|
date: () => SCALAR,
|
|
3597
3667
|
datetime: () => SCALAR,
|