@sanity/workflow-blueprint 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanity, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @sanity/workflow-blueprint
2
+
3
+ [Sanity Blueprints](https://www.sanity.io/docs/blueprints/blueprints-introduction)
4
+ integration for Editorial Workflows: declare a workflow definition deployment
5
+ as a blueprint resource, and provision it from a blueprint resource provider.
6
+
7
+ > [!NOTE]
8
+ > Early access, restricted — and deliberately in-between. The
9
+ > `sanity.workflow` resource type has no registered provider in the
10
+ > blueprints service yet: **do not add this resource to a real blueprint** —
11
+ > how the service treats an unregistered resource type is unverified and may
12
+ > fail the whole stack operation. Until the provider is registered, the
13
+ > workflow CLI (`sanity-workflows deploy`) remains the only working deploy
14
+ > path; what this package delivers today is the eager manifest-evaluation
15
+ > validation and the provider core that the registered provider will execute.
16
+
17
+ ## Declaring the resource
18
+
19
+ `defineWorkflows` takes the same deployment shape as one
20
+ `deployments[]` entry of a `sanity.workflow.ts` config, so an existing entry
21
+ drops in unchanged:
22
+
23
+ ```ts
24
+ // sanity.blueprint.ts
25
+ import {defineBlueprint} from '@sanity/blueprints'
26
+ import {defineWorkflows} from '@sanity/workflow-blueprint'
27
+ import config from './sanity.workflow'
28
+
29
+ export default defineBlueprint({
30
+ resources: [defineWorkflows(config.deployments[0])],
31
+ })
32
+ ```
33
+
34
+ The definer validates eagerly — definition structure, GROQ, and
35
+ `@<handle>:` alias bindings all fail at manifest evaluation, in the author's
36
+ terminal, never as a failed stack operation. The resulting resource is pure
37
+ data: the deployment rides the manifest verbatim, definitions in authored
38
+ form (alias expansion happens at provision).
39
+
40
+ Blueprints matches resources by `name` (default:
41
+ `editorial-workflows-<tag>`) — keep the name stable once instances exist.
42
+ Definitions are **retain-only** through blueprints: the default lifecycle is
43
+ `{deletionPolicy: 'retain'}` (removing the resource detaches the deployed
44
+ definitions instead of deleting them), and a policy that promises deletion
45
+ (`allow`, `replace`) is rejected at manifest evaluation.
46
+
47
+ ## The provider core
48
+
49
+ What a `blueprints-resource-workflow` provider runs per lifecycle phase:
50
+
51
+ ```ts
52
+ import {destroyWorkflowResource, provisionWorkflowResource} from '@sanity/workflow-blueprint'
53
+
54
+ // create and update are the same operation: the engine's definition deploy is
55
+ // content-addressed and create-only, so identical content is a no-op and any
56
+ // change mints the next immutable version.
57
+ await provisionWorkflowResource({resource, client})
58
+
59
+ // destroy always refuses: definitions are retain-only through blueprints, so
60
+ // an infrastructure teardown can never take editorial state with it. Deleting
61
+ // a definition is a deliberate act through the workflow CLI's
62
+ // `definition delete`, never a side effect of a manifest edit.
63
+ destroyWorkflowResource()
64
+ ```
65
+
66
+ `client` must be bound to the deployment's `workflowResource` with a token
67
+ that can write definition documents there. Provisioning parses and validates
68
+ the incoming resource at the boundary (`parseWorkflowResource`) — provider
69
+ input is JSON from the stack, never a typed value.
package/dist/index.cjs ADDED
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: !0
5
+ });
6
+
7
+ var blueprints = require("@sanity/blueprints"), workflowEngine = require("@sanity/workflow-engine"), define = require("@sanity/workflow-engine/define");
8
+
9
+ const WORKFLOW_RESOURCE_TYPE = "sanity.workflow";
10
+
11
+ function parseWorkflowDeployment(input, caller) {
12
+ const config = define.defineWorkflowConfig({
13
+ deployments: [ input ]
14
+ }), [deployment] = config.deployments;
15
+ if (deployment === void 0) throw new Error(`${caller}: expected a workflow deployment`);
16
+ workflowEngine.assertReaderModelAcknowledgement(deployment.expectedMinReaderModel, caller);
17
+ const definitions = deployment.definitions.map(def => workflowEngine.parseDefinitionInput(def, caller));
18
+ assertUniqueDefinitionNames(definitions, caller), definitions.forEach(workflowEngine.validateDefinition);
19
+ const aliases = workflowEngine.resourceAliasesToMap(deployment.resourceAliases);
20
+ for (const def of definitions) expandOrReattribute({
21
+ def: def,
22
+ aliases: aliases,
23
+ caller: caller
24
+ });
25
+ return assertAcyclicSpawnRefs(definitions, caller), {
26
+ ...deployment,
27
+ definitions: definitions
28
+ };
29
+ }
30
+
31
+ function assertUniqueDefinitionNames(definitions, caller) {
32
+ const seen = /* @__PURE__ */ new Set;
33
+ for (const def of definitions) {
34
+ if (seen.has(def.name)) throw new Error(`${caller}: duplicate definition name "${def.name}" in deployment`);
35
+ seen.add(def.name);
36
+ }
37
+ }
38
+
39
+ function expandOrReattribute({def: def, aliases: aliases, caller: caller}) {
40
+ try {
41
+ workflowEngine.expandResourceAliases(def, aliases);
42
+ } catch (error) {
43
+ throw new Error(workflowEngine.errorMessage(error).replace(/^workflow\.deployDefinitions: /, `${caller}: `), {
44
+ cause: error
45
+ });
46
+ }
47
+ }
48
+
49
+ function assertAcyclicSpawnRefs(definitions, caller) {
50
+ const remaining = new Map(definitions.map(def => [ def.name, def ]));
51
+ for (;remaining.size > 0; ) {
52
+ const referenced = new Set([ ...remaining.values() ].flatMap(def => workflowEngine.refsOf(def).map(ref => ref.name)).filter(name => remaining.has(name))), free = [ ...remaining.values() ].filter(def => !referenced.has(def.name));
53
+ if (free.length === 0) throw new Error(`${caller}: reference cycle among definitions ${[ ...remaining.keys() ].join(", ")}`);
54
+ for (const def of free) remaining.delete(def.name);
55
+ }
56
+ }
57
+
58
+ function defineWorkflows(deployment, options) {
59
+ const parsed = parseWorkflowDeployment(deployment, "defineWorkflows");
60
+ if (options?.name === "") throw new Error("defineWorkflows: `name` must be a non-empty string");
61
+ const policy = options?.lifecycle?.deletionPolicy;
62
+ if (policy === "allow" || policy === "replace") throw new Error(`defineWorkflows: deletionPolicy '${policy}' is not supported — Editorial Workflows definitions are retain-only through blueprints; delete deliberately with the workflow CLI's \`definition delete\`.`);
63
+ const resource = {
64
+ name: options?.name ?? `editorial-workflows-${parsed.tag}`,
65
+ type: WORKFLOW_RESOURCE_TYPE,
66
+ lifecycle: options?.lifecycle ?? {
67
+ deletionPolicy: "retain"
68
+ },
69
+ deployment: parsed
70
+ }, errors = blueprints.validateResource(resource);
71
+ if (errors.length > 0) throw new Error(`defineWorkflows: ${errors.map(error => error.message).join("; ")}`);
72
+ return resource;
73
+ }
74
+
75
+ function parseWorkflowResource(input) {
76
+ if (typeof input != "object" || input === null) throw new Error("workflow resource: expected an object");
77
+ const record = input;
78
+ if (record.type !== WORKFLOW_RESOURCE_TYPE) throw new Error(`workflow resource: expected type "${WORKFLOW_RESOURCE_TYPE}", got "${String(record.type)}"`);
79
+ if (typeof record.name != "string" || record.name === "") throw new Error("workflow resource: `name` must be a non-empty string");
80
+ const deployment = parseWorkflowDeployment(record.deployment, "workflow resource");
81
+ return {
82
+ ...record,
83
+ type: WORKFLOW_RESOURCE_TYPE,
84
+ name: record.name,
85
+ deployment: deployment
86
+ };
87
+ }
88
+
89
+ function engineScope({client: client, telemetry: telemetry}, deployment) {
90
+ return {
91
+ client: client,
92
+ tag: deployment.tag,
93
+ workflowResource: deployment.workflowResource,
94
+ ...telemetry === void 0 ? {} : {
95
+ telemetry: telemetry
96
+ }
97
+ };
98
+ }
99
+
100
+ async function provisionWorkflowResource({resource: resource, ...scope}) {
101
+ const {deployment: deployment} = parseWorkflowResource(resource);
102
+ return workflowEngine.workflow.deployDefinitions({
103
+ ...engineScope(scope, deployment),
104
+ expectedMinReaderModel: deployment.expectedMinReaderModel,
105
+ resourceAliases: workflowEngine.resourceAliasesToMap(deployment.resourceAliases),
106
+ definitions: deployment.definitions
107
+ });
108
+ }
109
+
110
+ function destroyWorkflowResource() {
111
+ throw new Error("workflow resource: destroy is not supported — Editorial Workflows definitions are retain-only through blueprints. Deployed definitions and their instances outlive the stack; delete deliberately with the workflow CLI's `definition delete`.");
112
+ }
113
+
114
+ exports.WORKFLOW_RESOURCE_TYPE = WORKFLOW_RESOURCE_TYPE;
115
+
116
+ exports.defineWorkflows = defineWorkflows;
117
+
118
+ exports.destroyWorkflowResource = destroyWorkflowResource;
119
+
120
+ exports.parseWorkflowDeployment = parseWorkflowDeployment;
121
+
122
+ exports.parseWorkflowResource = parseWorkflowResource;
123
+
124
+ exports.provisionWorkflowResource = provisionWorkflowResource;
@@ -0,0 +1,154 @@
1
+ import type { BlueprintResource } from "@sanity/blueprints";
2
+ import { BlueprintResourceLifecycle } from "@sanity/blueprints";
3
+ import { DeployDefinitionsResult } from "@sanity/workflow-engine";
4
+ import { WorkflowClient } from "@sanity/workflow-engine";
5
+ import { WorkflowDeployment } from "@sanity/workflow-engine";
6
+ import { WorkflowTelemetryLogger } from "@sanity/workflow-engine";
7
+
8
+ /**
9
+ * Declare an Editorial Workflows deployment as a Sanity Blueprints resource —
10
+ * the entry for a `sanity.blueprint.ts` `resources` array. Takes the same
11
+ * deployment shape as one `deployments[]` entry of a `sanity.workflow.ts`
12
+ * config, so an existing config entry drops in unchanged.
13
+ *
14
+ * Validates eagerly ({@link parseWorkflowDeployment}) so a bad definition or
15
+ * an unbound `@<handle>:` alias fails at manifest evaluation, before anything
16
+ * ships to the blueprint service.
17
+ *
18
+ * @remarks
19
+ * NOT deployable yet: the blueprints service has no registered
20
+ * `sanity.workflow` provider, and how it treats an unregistered resource type
21
+ * is unverified — adding this resource to a real blueprint may fail the whole
22
+ * stack operation. Until the provider is registered, deploy definitions with
23
+ * the workflow CLI (`sanity-workflows deploy`).
24
+ *
25
+ * ```ts
26
+ * // sanity.blueprint.ts
27
+ * import {defineBlueprint} from '@sanity/blueprints'
28
+ * import {defineWorkflows} from '@sanity/workflow-blueprint'
29
+ * import config from './sanity.workflow'
30
+ *
31
+ * export default defineBlueprint({
32
+ * resources: [defineWorkflows(config.deployments[0])],
33
+ * })
34
+ * ```
35
+ */
36
+ export declare function defineWorkflows(
37
+ deployment: WorkflowDeployment,
38
+ options?: DefineWorkflowsOptions,
39
+ ): EditorialWorkflowsResource;
40
+
41
+ export declare interface DefineWorkflowsOptions {
42
+ /**
43
+ * Blueprint resource name — the identity blueprints matches deploys on.
44
+ * Defaults to `editorial-workflows-<tag>`. Renaming a deployed resource is
45
+ * a destroy + create, so with live instances keep the name stable.
46
+ */
47
+ name?: string;
48
+ /**
49
+ * Blueprint lifecycle policy. Defaults to `{deletionPolicy: 'retain'}`:
50
+ * removing the resource (or destroying its stack) detaches the deployed
51
+ * definitions instead of deleting them. Definitions are retain-only through
52
+ * blueprints — a `deletionPolicy` that promises deletion (`allow`,
53
+ * `replace`) is rejected here, because the provider's destroy always
54
+ * refuses ({@link destroyWorkflowResource}) and honouring the policy would
55
+ * only defer that failure to the stack operation. `protect` is accepted
56
+ * (stricter than retain).
57
+ */
58
+ lifecycle?: BlueprintResourceLifecycle;
59
+ }
60
+
61
+ /**
62
+ * Refuses, always: Editorial Workflows definitions are retain-only through
63
+ * blueprints. Deployed definitions — immutable, versioned, with instances
64
+ * pinned to them — outlive any stack, so an infrastructure teardown can never
65
+ * take editorial state with it. Deleting a definition is a deliberate,
66
+ * per-definition act through the engine's `deleteDefinition` (the workflow
67
+ * CLI's `definition delete`), never a side effect of a manifest edit.
68
+ *
69
+ * The definer enforces the same posture eagerly by rejecting a
70
+ * `deletionPolicy` that promises deletion.
71
+ */
72
+ export declare function destroyWorkflowResource(): never;
73
+
74
+ /**
75
+ * An Editorial Workflows deployment declared as a Sanity Blueprints resource.
76
+ * Pure data: the blueprint manifest serializes it verbatim into the stack
77
+ * mutation, and the provider ({@link provisionWorkflowResource}) reads it back
78
+ * to run the definition deploy. Blueprints matches resources by `name`, so
79
+ * renaming one is a destroy + create, never an update.
80
+ */
81
+ export declare interface EditorialWorkflowsResource extends BlueprintResource {
82
+ type: typeof WORKFLOW_RESOURCE_TYPE;
83
+ /** The deployment to provision — the same shape as one `deployments[]`
84
+ * entry of a `sanity.workflow.ts` config. Definitions are carried in
85
+ * authored form; `@<handle>:` aliases expand at provision, not here. */
86
+ deployment: WorkflowDeployment;
87
+ }
88
+
89
+ /**
90
+ * Validate one deployment the way the definition deploy will, so every
91
+ * offline-checkable author error fails here — in the author's terminal at
92
+ * manifest evaluation — rather than as a failed stack operation: the
93
+ * deployment envelope (tag, resource binding, alias bindings) through the
94
+ * config schema, each definition boundary-parsed and validated, definition
95
+ * names checked for batch uniqueness, a dry alias expansion (fail-closed on
96
+ * an unbound `@<handle>:` reference), and the spawn-reference walk that
97
+ * rejects in-set cycles.
98
+ *
99
+ * Returns the parsed deployment (definition document envelopes stripped, so a
100
+ * fetched definition document round-trips cleanly).
101
+ */
102
+ export declare function parseWorkflowDeployment(
103
+ input: unknown,
104
+ caller: string,
105
+ ): WorkflowDeployment;
106
+
107
+ /**
108
+ * Parse an incoming blueprint resource document into a typed
109
+ * {@link EditorialWorkflowsResource}, validating the deployment exactly like
110
+ * the definer does. The boundary for provider lifecycle calls, whose input is
111
+ * JSON from the stack, never a typed value.
112
+ */
113
+ export declare function parseWorkflowResource(
114
+ input: unknown,
115
+ ): EditorialWorkflowsResource;
116
+
117
+ /**
118
+ * Provision (create OR update) a workflow resource: run the engine's
119
+ * definition deploy for the resource's deployment. One function for both
120
+ * lifecycle phases because the deploy is content-addressed and create-only —
121
+ * identical content is a per-definition no-op (`unchanged`), any change mints
122
+ * the next immutable version, and prior versions (with the instances pinned
123
+ * to them) are never touched. Re-running after a partial failure converges.
124
+ *
125
+ * A definition REMOVED from the deployment since the last provision is
126
+ * retained, not deleted — deployed definitions only ever leave the lake
127
+ * through the engine's explicit `deleteDefinition` (the workflow CLI's
128
+ * `definition delete`), never through the blueprint lifecycle
129
+ * ({@link destroyWorkflowResource}).
130
+ */
131
+ export declare function provisionWorkflowResource({
132
+ resource,
133
+ ...scope
134
+ }: WorkflowResourceScope): Promise<DeployDefinitionsResult>;
135
+
136
+ /**
137
+ * The blueprint resource type string for an Editorial Workflows definition
138
+ * deployment. A single constant so the manifest, the provider, and any future
139
+ * first-class `@sanity/blueprints` definer agree on one name.
140
+ */
141
+ export declare const WORKFLOW_RESOURCE_TYPE = "sanity.workflow";
142
+
143
+ /**
144
+ * One provider lifecycle call's scope: the resource document as it arrives
145
+ * from the blueprint service (parsed and validated at this boundary) and a
146
+ * client bound to the deployment's `workflowResource`.
147
+ */
148
+ export declare interface WorkflowResourceScope {
149
+ resource: unknown;
150
+ client: WorkflowClient;
151
+ telemetry?: WorkflowTelemetryLogger;
152
+ }
153
+
154
+ export {};
@@ -0,0 +1,154 @@
1
+ import type { BlueprintResource } from "@sanity/blueprints";
2
+ import { BlueprintResourceLifecycle } from "@sanity/blueprints";
3
+ import { DeployDefinitionsResult } from "@sanity/workflow-engine";
4
+ import { WorkflowClient } from "@sanity/workflow-engine";
5
+ import { WorkflowDeployment } from "@sanity/workflow-engine";
6
+ import { WorkflowTelemetryLogger } from "@sanity/workflow-engine";
7
+
8
+ /**
9
+ * Declare an Editorial Workflows deployment as a Sanity Blueprints resource —
10
+ * the entry for a `sanity.blueprint.ts` `resources` array. Takes the same
11
+ * deployment shape as one `deployments[]` entry of a `sanity.workflow.ts`
12
+ * config, so an existing config entry drops in unchanged.
13
+ *
14
+ * Validates eagerly ({@link parseWorkflowDeployment}) so a bad definition or
15
+ * an unbound `@<handle>:` alias fails at manifest evaluation, before anything
16
+ * ships to the blueprint service.
17
+ *
18
+ * @remarks
19
+ * NOT deployable yet: the blueprints service has no registered
20
+ * `sanity.workflow` provider, and how it treats an unregistered resource type
21
+ * is unverified — adding this resource to a real blueprint may fail the whole
22
+ * stack operation. Until the provider is registered, deploy definitions with
23
+ * the workflow CLI (`sanity-workflows deploy`).
24
+ *
25
+ * ```ts
26
+ * // sanity.blueprint.ts
27
+ * import {defineBlueprint} from '@sanity/blueprints'
28
+ * import {defineWorkflows} from '@sanity/workflow-blueprint'
29
+ * import config from './sanity.workflow'
30
+ *
31
+ * export default defineBlueprint({
32
+ * resources: [defineWorkflows(config.deployments[0])],
33
+ * })
34
+ * ```
35
+ */
36
+ export declare function defineWorkflows(
37
+ deployment: WorkflowDeployment,
38
+ options?: DefineWorkflowsOptions,
39
+ ): EditorialWorkflowsResource;
40
+
41
+ export declare interface DefineWorkflowsOptions {
42
+ /**
43
+ * Blueprint resource name — the identity blueprints matches deploys on.
44
+ * Defaults to `editorial-workflows-<tag>`. Renaming a deployed resource is
45
+ * a destroy + create, so with live instances keep the name stable.
46
+ */
47
+ name?: string;
48
+ /**
49
+ * Blueprint lifecycle policy. Defaults to `{deletionPolicy: 'retain'}`:
50
+ * removing the resource (or destroying its stack) detaches the deployed
51
+ * definitions instead of deleting them. Definitions are retain-only through
52
+ * blueprints — a `deletionPolicy` that promises deletion (`allow`,
53
+ * `replace`) is rejected here, because the provider's destroy always
54
+ * refuses ({@link destroyWorkflowResource}) and honouring the policy would
55
+ * only defer that failure to the stack operation. `protect` is accepted
56
+ * (stricter than retain).
57
+ */
58
+ lifecycle?: BlueprintResourceLifecycle;
59
+ }
60
+
61
+ /**
62
+ * Refuses, always: Editorial Workflows definitions are retain-only through
63
+ * blueprints. Deployed definitions — immutable, versioned, with instances
64
+ * pinned to them — outlive any stack, so an infrastructure teardown can never
65
+ * take editorial state with it. Deleting a definition is a deliberate,
66
+ * per-definition act through the engine's `deleteDefinition` (the workflow
67
+ * CLI's `definition delete`), never a side effect of a manifest edit.
68
+ *
69
+ * The definer enforces the same posture eagerly by rejecting a
70
+ * `deletionPolicy` that promises deletion.
71
+ */
72
+ export declare function destroyWorkflowResource(): never;
73
+
74
+ /**
75
+ * An Editorial Workflows deployment declared as a Sanity Blueprints resource.
76
+ * Pure data: the blueprint manifest serializes it verbatim into the stack
77
+ * mutation, and the provider ({@link provisionWorkflowResource}) reads it back
78
+ * to run the definition deploy. Blueprints matches resources by `name`, so
79
+ * renaming one is a destroy + create, never an update.
80
+ */
81
+ export declare interface EditorialWorkflowsResource extends BlueprintResource {
82
+ type: typeof WORKFLOW_RESOURCE_TYPE;
83
+ /** The deployment to provision — the same shape as one `deployments[]`
84
+ * entry of a `sanity.workflow.ts` config. Definitions are carried in
85
+ * authored form; `@<handle>:` aliases expand at provision, not here. */
86
+ deployment: WorkflowDeployment;
87
+ }
88
+
89
+ /**
90
+ * Validate one deployment the way the definition deploy will, so every
91
+ * offline-checkable author error fails here — in the author's terminal at
92
+ * manifest evaluation — rather than as a failed stack operation: the
93
+ * deployment envelope (tag, resource binding, alias bindings) through the
94
+ * config schema, each definition boundary-parsed and validated, definition
95
+ * names checked for batch uniqueness, a dry alias expansion (fail-closed on
96
+ * an unbound `@<handle>:` reference), and the spawn-reference walk that
97
+ * rejects in-set cycles.
98
+ *
99
+ * Returns the parsed deployment (definition document envelopes stripped, so a
100
+ * fetched definition document round-trips cleanly).
101
+ */
102
+ export declare function parseWorkflowDeployment(
103
+ input: unknown,
104
+ caller: string,
105
+ ): WorkflowDeployment;
106
+
107
+ /**
108
+ * Parse an incoming blueprint resource document into a typed
109
+ * {@link EditorialWorkflowsResource}, validating the deployment exactly like
110
+ * the definer does. The boundary for provider lifecycle calls, whose input is
111
+ * JSON from the stack, never a typed value.
112
+ */
113
+ export declare function parseWorkflowResource(
114
+ input: unknown,
115
+ ): EditorialWorkflowsResource;
116
+
117
+ /**
118
+ * Provision (create OR update) a workflow resource: run the engine's
119
+ * definition deploy for the resource's deployment. One function for both
120
+ * lifecycle phases because the deploy is content-addressed and create-only —
121
+ * identical content is a per-definition no-op (`unchanged`), any change mints
122
+ * the next immutable version, and prior versions (with the instances pinned
123
+ * to them) are never touched. Re-running after a partial failure converges.
124
+ *
125
+ * A definition REMOVED from the deployment since the last provision is
126
+ * retained, not deleted — deployed definitions only ever leave the lake
127
+ * through the engine's explicit `deleteDefinition` (the workflow CLI's
128
+ * `definition delete`), never through the blueprint lifecycle
129
+ * ({@link destroyWorkflowResource}).
130
+ */
131
+ export declare function provisionWorkflowResource({
132
+ resource,
133
+ ...scope
134
+ }: WorkflowResourceScope): Promise<DeployDefinitionsResult>;
135
+
136
+ /**
137
+ * The blueprint resource type string for an Editorial Workflows definition
138
+ * deployment. A single constant so the manifest, the provider, and any future
139
+ * first-class `@sanity/blueprints` definer agree on one name.
140
+ */
141
+ export declare const WORKFLOW_RESOURCE_TYPE = "sanity.workflow";
142
+
143
+ /**
144
+ * One provider lifecycle call's scope: the resource document as it arrives
145
+ * from the blueprint service (parsed and validated at this boundary) and a
146
+ * client bound to the deployment's `workflowResource`.
147
+ */
148
+ export declare interface WorkflowResourceScope {
149
+ resource: unknown;
150
+ client: WorkflowClient;
151
+ telemetry?: WorkflowTelemetryLogger;
152
+ }
153
+
154
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,112 @@
1
+ import { validateResource } from "@sanity/blueprints";
2
+
3
+ import { assertReaderModelAcknowledgement, parseDefinitionInput, validateDefinition, resourceAliasesToMap, expandResourceAliases, errorMessage, refsOf, workflow } from "@sanity/workflow-engine";
4
+
5
+ import { defineWorkflowConfig } from "@sanity/workflow-engine/define";
6
+
7
+ const WORKFLOW_RESOURCE_TYPE = "sanity.workflow";
8
+
9
+ function parseWorkflowDeployment(input, caller) {
10
+ const config = defineWorkflowConfig({
11
+ deployments: [ input ]
12
+ }), [deployment] = config.deployments;
13
+ if (deployment === void 0) throw new Error(`${caller}: expected a workflow deployment`);
14
+ assertReaderModelAcknowledgement(deployment.expectedMinReaderModel, caller);
15
+ const definitions = deployment.definitions.map(def => parseDefinitionInput(def, caller));
16
+ assertUniqueDefinitionNames(definitions, caller), definitions.forEach(validateDefinition);
17
+ const aliases = resourceAliasesToMap(deployment.resourceAliases);
18
+ for (const def of definitions) expandOrReattribute({
19
+ def: def,
20
+ aliases: aliases,
21
+ caller: caller
22
+ });
23
+ return assertAcyclicSpawnRefs(definitions, caller), {
24
+ ...deployment,
25
+ definitions: definitions
26
+ };
27
+ }
28
+
29
+ function assertUniqueDefinitionNames(definitions, caller) {
30
+ const seen = /* @__PURE__ */ new Set;
31
+ for (const def of definitions) {
32
+ if (seen.has(def.name)) throw new Error(`${caller}: duplicate definition name "${def.name}" in deployment`);
33
+ seen.add(def.name);
34
+ }
35
+ }
36
+
37
+ function expandOrReattribute({def: def, aliases: aliases, caller: caller}) {
38
+ try {
39
+ expandResourceAliases(def, aliases);
40
+ } catch (error) {
41
+ throw new Error(errorMessage(error).replace(/^workflow\.deployDefinitions: /, `${caller}: `), {
42
+ cause: error
43
+ });
44
+ }
45
+ }
46
+
47
+ function assertAcyclicSpawnRefs(definitions, caller) {
48
+ const remaining = new Map(definitions.map(def => [ def.name, def ]));
49
+ for (;remaining.size > 0; ) {
50
+ const referenced = new Set([ ...remaining.values() ].flatMap(def => refsOf(def).map(ref => ref.name)).filter(name => remaining.has(name))), free = [ ...remaining.values() ].filter(def => !referenced.has(def.name));
51
+ if (free.length === 0) throw new Error(`${caller}: reference cycle among definitions ${[ ...remaining.keys() ].join(", ")}`);
52
+ for (const def of free) remaining.delete(def.name);
53
+ }
54
+ }
55
+
56
+ function defineWorkflows(deployment, options) {
57
+ const parsed = parseWorkflowDeployment(deployment, "defineWorkflows");
58
+ if (options?.name === "") throw new Error("defineWorkflows: `name` must be a non-empty string");
59
+ const policy = options?.lifecycle?.deletionPolicy;
60
+ if (policy === "allow" || policy === "replace") throw new Error(`defineWorkflows: deletionPolicy '${policy}' is not supported — Editorial Workflows definitions are retain-only through blueprints; delete deliberately with the workflow CLI's \`definition delete\`.`);
61
+ const resource = {
62
+ name: options?.name ?? `editorial-workflows-${parsed.tag}`,
63
+ type: WORKFLOW_RESOURCE_TYPE,
64
+ lifecycle: options?.lifecycle ?? {
65
+ deletionPolicy: "retain"
66
+ },
67
+ deployment: parsed
68
+ }, errors = validateResource(resource);
69
+ if (errors.length > 0) throw new Error(`defineWorkflows: ${errors.map(error => error.message).join("; ")}`);
70
+ return resource;
71
+ }
72
+
73
+ function parseWorkflowResource(input) {
74
+ if (typeof input != "object" || input === null) throw new Error("workflow resource: expected an object");
75
+ const record = input;
76
+ if (record.type !== WORKFLOW_RESOURCE_TYPE) throw new Error(`workflow resource: expected type "${WORKFLOW_RESOURCE_TYPE}", got "${String(record.type)}"`);
77
+ if (typeof record.name != "string" || record.name === "") throw new Error("workflow resource: `name` must be a non-empty string");
78
+ const deployment = parseWorkflowDeployment(record.deployment, "workflow resource");
79
+ return {
80
+ ...record,
81
+ type: WORKFLOW_RESOURCE_TYPE,
82
+ name: record.name,
83
+ deployment: deployment
84
+ };
85
+ }
86
+
87
+ function engineScope({client: client, telemetry: telemetry}, deployment) {
88
+ return {
89
+ client: client,
90
+ tag: deployment.tag,
91
+ workflowResource: deployment.workflowResource,
92
+ ...telemetry === void 0 ? {} : {
93
+ telemetry: telemetry
94
+ }
95
+ };
96
+ }
97
+
98
+ async function provisionWorkflowResource({resource: resource, ...scope}) {
99
+ const {deployment: deployment} = parseWorkflowResource(resource);
100
+ return workflow.deployDefinitions({
101
+ ...engineScope(scope, deployment),
102
+ expectedMinReaderModel: deployment.expectedMinReaderModel,
103
+ resourceAliases: resourceAliasesToMap(deployment.resourceAliases),
104
+ definitions: deployment.definitions
105
+ });
106
+ }
107
+
108
+ function destroyWorkflowResource() {
109
+ throw new Error("workflow resource: destroy is not supported — Editorial Workflows definitions are retain-only through blueprints. Deployed definitions and their instances outlive the stack; delete deliberately with the workflow CLI's `definition delete`.");
110
+ }
111
+
112
+ export { WORKFLOW_RESOURCE_TYPE, defineWorkflows, destroyWorkflowResource, parseWorkflowDeployment, parseWorkflowResource, provisionWorkflowResource };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@sanity/workflow-blueprint",
3
+ "version": "0.0.0",
4
+ "description": "Sanity Blueprints integration for Editorial Workflows: the workflow resource definer and the provider core that provisions definition deploys. Early access — not deployable until the sanity.workflow provider is registered in the blueprints service.",
5
+ "keywords": [
6
+ "blueprints",
7
+ "sanity",
8
+ "sanity-io",
9
+ "workflow",
10
+ "workflows"
11
+ ],
12
+ "homepage": "https://github.com/sanity-io/workflows/tree/main/packages/workflow-blueprint#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/sanity-io/workflows/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Sanity.io <hello@sanity.io>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/sanity-io/workflows.git",
21
+ "directory": "packages/workflow-blueprint"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@sanity/blueprints": "^0.21.0",
45
+ "@sanity/workflow-engine": "0.17.0"
46
+ },
47
+ "devDependencies": {
48
+ "@sanity/pkg-utils": "^10.5.2",
49
+ "@types/node": "^25.9.1",
50
+ "vitest": "^4.1.8",
51
+ "@sanity/workflow-engine-test": "0.12.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "scripts": {
57
+ "build": "pkg-utils build --clean",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "typecheck": "tsc --noEmit -p tsconfig.json"
61
+ }
62
+ }