@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 +21 -0
- package/README.md +69 -0
- package/dist/index.cjs +124 -0
- package/dist/index.d.cts +154 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +112 -0
- package/package.json +62 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|