@sanity/workflow-cli 0.8.0 → 0.9.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/README.md +99 -61
- package/dist/commands/abort.d.ts +5 -7
- package/dist/commands/abort.js +15 -14
- package/dist/commands/definition/delete.d.ts +10 -6
- package/dist/commands/definition/delete.js +8 -11
- package/dist/commands/definition/diff.d.ts +8 -1
- package/dist/commands/definition/diff.js +30 -24
- package/dist/commands/definition/list.d.ts +4 -4
- package/dist/commands/definition/list.js +8 -8
- package/dist/commands/definition/show.d.ts +0 -10
- package/dist/commands/definition/show.js +11 -20
- package/dist/commands/deploy.d.ts +58 -5
- package/dist/commands/deploy.js +134 -27
- package/dist/commands/diagnose.d.ts +5 -1
- package/dist/commands/diagnose.js +18 -28
- package/dist/commands/fire-action.d.ts +15 -28
- package/dist/commands/fire-action.js +37 -72
- package/dist/commands/list.d.ts +2 -2
- package/dist/commands/list.js +8 -8
- package/dist/commands/{retry-activity.d.ts → reset-activity.d.ts} +1 -1
- package/dist/commands/{retry-activity.js → reset-activity.js} +2 -2
- package/dist/commands/set-stage.d.ts +27 -3
- package/dist/commands/set-stage.js +80 -13
- package/dist/commands/show.d.ts +1 -0
- package/dist/commands/show.js +14 -8
- package/dist/commands/start.d.ts +59 -0
- package/dist/commands/start.js +169 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.js +9 -5
- package/dist/lib/client.d.ts +11 -14
- package/dist/lib/client.js +45 -33
- package/dist/lib/context.d.ts +62 -0
- package/dist/lib/context.js +82 -0
- package/dist/lib/definitions.d.ts +38 -29
- package/dist/lib/definitions.js +45 -85
- package/dist/lib/diff.js +8 -0
- package/dist/lib/env.d.ts +0 -6
- package/dist/lib/env.js +3 -9
- package/dist/lib/fail.d.ts +6 -0
- package/dist/lib/fail.js +11 -1
- package/dist/lib/flags.d.ts +14 -2
- package/dist/lib/flags.js +24 -3
- package/dist/lib/load-config.d.ts +11 -0
- package/dist/lib/load-config.js +69 -0
- package/dist/lib/operation-args.d.ts +25 -7
- package/dist/lib/operation-args.js +12 -11
- package/dist/lib/ops-report.d.ts +19 -7
- package/dist/lib/ops-report.js +6 -7
- package/dist/lib/params.d.ts +7 -0
- package/dist/lib/params.js +26 -0
- package/dist/lib/select-deployment.d.ts +31 -0
- package/dist/lib/select-deployment.js +58 -0
- package/dist/lib/ui.d.ts +3 -1
- package/dist/lib/ui.js +1 -3
- package/oclif.manifest.json +145 -86
- package/package.json +6 -4
- package/dist/commands/move-stage.d.ts +0 -47
- package/dist/commands/move-stage.js +0 -77
- package/dist/lib/config.d.ts +0 -18
- package/dist/lib/config.js +0 -50
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
3
|
+
import { isStartableDefinition, workflow, } from '@sanity/workflow-engine';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { resolveContext } from "../lib/context.js";
|
|
6
|
+
import { fetchDeployedDefinition } from "../lib/definitions.js";
|
|
7
|
+
import { errorMessage, fail, failOnThrow } from "../lib/fail.js";
|
|
8
|
+
import { actorFlags, jsonFlags, tagFlags } from "../lib/flags.js";
|
|
9
|
+
import { baseEngineArgs, resolveActor } from "../lib/operation-args.js";
|
|
10
|
+
import { parseParams } from "../lib/params.js";
|
|
11
|
+
export default class Start extends Command {
|
|
12
|
+
static description = 'Start a workflow instance from a deployed definition. Supply values for the ' +
|
|
13
|
+
"workflow's input-sourced fields with --field (e.g. the subject document ref).";
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> start productLaunch',
|
|
16
|
+
'<%= config.bin %> start article-review --field subject=\'{"id":"dataset:proj:ds:article-1","type":"article"}\'',
|
|
17
|
+
'<%= config.bin %> start article-review --as user.xyz --as-role editor',
|
|
18
|
+
'<%= config.bin %> start productLaunch --version 2 --tag prod',
|
|
19
|
+
];
|
|
20
|
+
static args = {
|
|
21
|
+
name: Args.string({ required: true, description: 'Workflow definition name.' }),
|
|
22
|
+
};
|
|
23
|
+
static flags = {
|
|
24
|
+
...tagFlags,
|
|
25
|
+
version: Flags.integer({
|
|
26
|
+
description: 'Definition version to start from (default: highest deployed).',
|
|
27
|
+
}),
|
|
28
|
+
field: Flags.string({
|
|
29
|
+
description: 'Initial value for a declared input-sourced field, as name=value (repeatable). Values ' +
|
|
30
|
+
'are JSON-parsed, falling back to a string; ref kinds take a JSON object with a GDR ' +
|
|
31
|
+
'`id` and doc `type`.',
|
|
32
|
+
multiple: true,
|
|
33
|
+
default: [],
|
|
34
|
+
}),
|
|
35
|
+
...actorFlags,
|
|
36
|
+
...jsonFlags,
|
|
37
|
+
};
|
|
38
|
+
async run() {
|
|
39
|
+
const { args, flags } = await this.parse(Start);
|
|
40
|
+
const { deployment, client } = await resolveContext(flags);
|
|
41
|
+
const actor = failOnThrow('start usage error:', () => resolveActor(flags.as, flags['as-role']));
|
|
42
|
+
const values = failOnThrow('Invalid --field:', () => parseParams(flags.field));
|
|
43
|
+
const initialFields = await resolveStartInputs({
|
|
44
|
+
client,
|
|
45
|
+
name: args.name,
|
|
46
|
+
tag: deployment.tag,
|
|
47
|
+
version: flags.version,
|
|
48
|
+
values,
|
|
49
|
+
});
|
|
50
|
+
const startArgs = {
|
|
51
|
+
client,
|
|
52
|
+
...buildStartArgs({
|
|
53
|
+
scope: deployment,
|
|
54
|
+
name: args.name,
|
|
55
|
+
version: flags.version,
|
|
56
|
+
initialFields,
|
|
57
|
+
actor,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
if (flags.json) {
|
|
61
|
+
const result = await workflow
|
|
62
|
+
.startInstance(startArgs)
|
|
63
|
+
.catch((error) => fail('start error:', errorMessage(error)));
|
|
64
|
+
this.log(JSON.stringify(startResultJson(result.instance), null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const spinner = ora(`Starting ${args.name}…`).start();
|
|
68
|
+
try {
|
|
69
|
+
const result = await workflow.startInstance(startArgs);
|
|
70
|
+
spinner.succeed(startReport(result.instance));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
spinner.fail('Start rejected');
|
|
74
|
+
fail('start error:', errorMessage(error));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Everything `start` must resolve before the engine call: fetch the deployed
|
|
80
|
+
* definition (failing when nothing is deployed under the name), refuse
|
|
81
|
+
* non-startable ones, and type the operator's `--field` values against the
|
|
82
|
+
* declared entries.
|
|
83
|
+
*/
|
|
84
|
+
async function resolveStartInputs({ client, name, tag, version, values, }) {
|
|
85
|
+
const deployed = await fetchDeployedDefinition({ client, name, tag, version });
|
|
86
|
+
if (deployed === undefined) {
|
|
87
|
+
fail(`No deployed definition "${name}".`);
|
|
88
|
+
}
|
|
89
|
+
const refusal = startRefusal(deployed);
|
|
90
|
+
if (refusal !== undefined) {
|
|
91
|
+
fail('start error:', refusal);
|
|
92
|
+
}
|
|
93
|
+
return failOnThrow('start usage error:', () => buildInitialFields({ declared: deployed.fields ?? [], values }));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Why a definition can't be started standalone, or `undefined` when it can.
|
|
97
|
+
* Advisory, mirroring the Startable column in `definition list` — the engine
|
|
98
|
+
* itself does not refuse ({@link isStartableDefinition}), but the CLI has no
|
|
99
|
+
* way to supply the parent context a spawn-only child expects.
|
|
100
|
+
*/
|
|
101
|
+
export function startRefusal(definition) {
|
|
102
|
+
if (isStartableDefinition(definition)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return "definition is spawn-only (lifecycle: 'child') — instances of it are spawned by a parent workflow's activity, not started standalone";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Type each `--field name=value` against the workflow's declared field
|
|
109
|
+
* entries — the engine takes typed {@link InitialFieldValue}s, and a value's
|
|
110
|
+
* type IS its declared entry's kind. Only `input`-sourced entries read
|
|
111
|
+
* caller values (the engine silently ignores the rest), so anything else
|
|
112
|
+
* fails here, naming the fields that ARE settable; value validation stays
|
|
113
|
+
* in the engine.
|
|
114
|
+
*/
|
|
115
|
+
export function buildInitialFields({ declared, values, }) {
|
|
116
|
+
const settable = declared.filter((f) => f.initialValue?.type === 'input');
|
|
117
|
+
const settableNames = settable.map((f) => f.name).join(', ');
|
|
118
|
+
return Object.entries(values).map(([name, value]) => {
|
|
119
|
+
const entry = declared.find((f) => f.name === name);
|
|
120
|
+
if (entry === undefined) {
|
|
121
|
+
throw new Error(settable.length > 0
|
|
122
|
+
? `workflow declares no field "${name}" — input fields: ${settableNames}`
|
|
123
|
+
: `workflow declares no field "${name}" — it declares no input fields at all`);
|
|
124
|
+
}
|
|
125
|
+
if (entry.initialValue?.type !== 'input') {
|
|
126
|
+
throw new Error(`field "${name}" is not input-sourced — the engine would silently ignore the value. ` +
|
|
127
|
+
(settable.length > 0
|
|
128
|
+
? `Input fields: ${settableNames}`
|
|
129
|
+
: 'This workflow has no input fields.'));
|
|
130
|
+
}
|
|
131
|
+
// Operator-supplied JSON; the engine validates the value against the
|
|
132
|
+
// declared kind at start, so this cast is the CLI/engine boundary.
|
|
133
|
+
return { type: entry.type, name, value };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The shared engine args ({@link baseEngineArgs}) plus the start-specific
|
|
138
|
+
* fields. Optionals spread conditionally because the engine's optional args
|
|
139
|
+
* reject an explicit `undefined` under `exactOptionalPropertyTypes`.
|
|
140
|
+
*/
|
|
141
|
+
export function buildStartArgs({ scope, name, version, initialFields, actor, }) {
|
|
142
|
+
return {
|
|
143
|
+
...baseEngineArgs(scope, actor),
|
|
144
|
+
definition: name,
|
|
145
|
+
...(version !== undefined ? { version } : {}),
|
|
146
|
+
...(initialFields.length > 0 ? { initialFields } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The success spinner message. Pure so the in-flight / immediately-terminal
|
|
150
|
+
* permutations can be asserted without driving a real start. */
|
|
151
|
+
export function startReport(instance) {
|
|
152
|
+
const id = styleText('bold', instance._id);
|
|
153
|
+
const stage = styleText('bold', instance.currentStage);
|
|
154
|
+
return instance.completedAt !== undefined
|
|
155
|
+
? `Started ${id} — completed immediately at ${stage}`
|
|
156
|
+
: `Started ${id} — now at ${stage}`;
|
|
157
|
+
}
|
|
158
|
+
/** The `--json` payload. Keys follow the CLI's `--json` vocabulary (matching
|
|
159
|
+
* `fire-action`): `instanceId` carries the document `_id`, `version` the
|
|
160
|
+
* pinned definition version; the rest mirror the instance fields. */
|
|
161
|
+
export function startResultJson(instance) {
|
|
162
|
+
return {
|
|
163
|
+
instanceId: instance._id,
|
|
164
|
+
definition: instance.definition,
|
|
165
|
+
version: instance.pinnedVersion,
|
|
166
|
+
currentStage: instance.currentStage,
|
|
167
|
+
...(instance.completedAt !== undefined ? { completedAt: instance.completedAt } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
package/dist/commands/tail.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export default class Tail extends Command {
|
|
|
5
5
|
static args: {
|
|
6
6
|
instanceId: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
|
+
static flags: {
|
|
9
|
+
tag: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
8
11
|
run(): Promise<void>;
|
|
9
12
|
/**
|
|
10
13
|
* Fetch the current instance state, print history entries past the
|
package/dist/commands/tail.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { Args, Command } from '@oclif/core';
|
|
3
3
|
import logSymbols from 'log-symbols';
|
|
4
|
-
import {
|
|
4
|
+
import { resolveReadContext } from "../lib/context.js";
|
|
5
5
|
import { fail } from "../lib/fail.js";
|
|
6
|
+
import { tagFlags } from "../lib/flags.js";
|
|
6
7
|
import { formatTimestamp } from "../lib/ui.js";
|
|
7
8
|
export default class Tail extends Command {
|
|
8
9
|
static description = 'Stream new history entries on a workflow instance as they land in the dataset.';
|
|
@@ -13,9 +14,12 @@ export default class Tail extends Command {
|
|
|
13
14
|
description: 'Workflow instance id to tail.',
|
|
14
15
|
}),
|
|
15
16
|
};
|
|
17
|
+
static flags = {
|
|
18
|
+
...tagFlags,
|
|
19
|
+
};
|
|
16
20
|
async run() {
|
|
17
|
-
const { args } = await this.parse(Tail);
|
|
18
|
-
const client =
|
|
21
|
+
const { args, flags } = await this.parse(Tail);
|
|
22
|
+
const { client } = await resolveReadContext(flags);
|
|
19
23
|
const initial = await client.getDocument(args.instanceId);
|
|
20
24
|
if (!initial) {
|
|
21
25
|
fail(`No instance with id "${args.instanceId}"`);
|
|
@@ -33,7 +37,7 @@ export default class Tail extends Command {
|
|
|
33
37
|
.subscribe({
|
|
34
38
|
next: async () => {
|
|
35
39
|
try {
|
|
36
|
-
seen = await this.printNewEntries(client, args.instanceId, seen);
|
|
40
|
+
seen = await this.printNewEntries({ client, instanceId: args.instanceId, seen });
|
|
37
41
|
}
|
|
38
42
|
catch (err) {
|
|
39
43
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -61,7 +65,7 @@ export default class Tail extends Command {
|
|
|
61
65
|
* given high-water mark, return the new mark. Pulled out so the
|
|
62
66
|
* async work in the subscription's `next` handler stays tidy.
|
|
63
67
|
*/
|
|
64
|
-
async printNewEntries(client, instanceId, seen) {
|
|
68
|
+
async printNewEntries({ client, instanceId, seen, }) {
|
|
65
69
|
const next = await client.getDocument(instanceId);
|
|
66
70
|
if (!next)
|
|
67
71
|
return seen;
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { type SanityClient } from '@sanity/client';
|
|
2
|
-
|
|
3
|
-
projectId: string;
|
|
4
|
-
dataset: string;
|
|
5
|
-
token: string;
|
|
6
|
-
apiHost: string;
|
|
7
|
-
apiVersion: string;
|
|
8
|
-
}
|
|
9
|
-
export declare function buildClient(env?: ClientEnv): SanityClient;
|
|
2
|
+
import { type WorkflowResource } from '@sanity/workflow-engine';
|
|
10
3
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* callers that supply their own {@link ClientEnv}.
|
|
4
|
+
* Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
|
|
5
|
+
* scripted use); otherwise fall back to the `sanity login` session token.
|
|
6
|
+
* Throws when neither exists rather than building an unauthenticated client
|
|
7
|
+
* that only 401s once it hits the API.
|
|
16
8
|
*/
|
|
17
|
-
export declare function
|
|
9
|
+
export declare function resolveToken(): Promise<string>;
|
|
10
|
+
/** {@link resolveToken}, funnelling the no-token case through the clean
|
|
11
|
+
* {@link fail} path rather than letting it surface as an oclif stack. */
|
|
12
|
+
export declare function resolveTokenOrFail(): Promise<string>;
|
|
13
|
+
/** Build a client for a workflow resource, authenticated with `token`. */
|
|
14
|
+
export declare function clientFor(resource: WorkflowResource, token: string): SanityClient;
|
package/dist/lib/client.js
CHANGED
|
@@ -1,40 +1,52 @@
|
|
|
1
|
+
import { getCliToken, isStaging } from '@sanity/cli-core';
|
|
1
2
|
import { createClient } from '@sanity/client';
|
|
3
|
+
import { datasetResourceParts, ENGINE_API_VERSION, } from '@sanity/workflow-engine';
|
|
2
4
|
import { ENV } from "./env.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { fail } from "./fail.js";
|
|
6
|
+
const API_VERSION = ENGINE_API_VERSION;
|
|
7
|
+
const STAGING_API_HOST = 'https://api.sanity.work';
|
|
8
|
+
/**
|
|
9
|
+
* The API host to hit: an explicit `SANITY_API_HOST` wins; otherwise mirror
|
|
10
|
+
* the Sanity CLI — staging routes to the staging API, prod falls through to
|
|
11
|
+
* `@sanity/client`'s default. Pairs with {@link resolveToken}, which reads the
|
|
12
|
+
* matching session token.
|
|
13
|
+
*/
|
|
14
|
+
function resolveApiHost() {
|
|
15
|
+
return process.env[ENV.SANITY_API_HOST] ?? (isStaging() ? STAGING_API_HOST : undefined);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the Sanity auth token. An explicit `SANITY_AUTH_TOKEN` wins (CI /
|
|
19
|
+
* scripted use); otherwise fall back to the `sanity login` session token.
|
|
20
|
+
* Throws when neither exists rather than building an unauthenticated client
|
|
21
|
+
* that only 401s once it hits the API.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveToken() {
|
|
24
|
+
const fromEnv = process.env[ENV.SANITY_AUTH_TOKEN]?.trim();
|
|
25
|
+
if (fromEnv) {
|
|
26
|
+
return fromEnv;
|
|
9
27
|
}
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
const fromSession = await getCliToken();
|
|
29
|
+
if (fromSession) {
|
|
30
|
+
return fromSession;
|
|
12
31
|
}
|
|
13
|
-
|
|
14
|
-
projectId,
|
|
15
|
-
dataset: process.env[ENV.SANITY_DATASET] ?? 'production',
|
|
16
|
-
token,
|
|
17
|
-
apiHost: process.env[ENV.SANITY_API_HOST] ?? 'https://api.sanity.io',
|
|
18
|
-
apiVersion: '2026-04-29',
|
|
19
|
-
};
|
|
32
|
+
throw new Error('No Sanity token found — run `sanity login`, or set SANITY_AUTH_TOKEN.');
|
|
20
33
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
apiVersion: env.apiVersion,
|
|
26
|
-
token: env.token,
|
|
27
|
-
apiHost: env.apiHost,
|
|
28
|
-
useCdn: false,
|
|
29
|
-
});
|
|
34
|
+
/** {@link resolveToken}, funnelling the no-token case through the clean
|
|
35
|
+
* {@link fail} path rather than letting it surface as an oclif stack. */
|
|
36
|
+
export async function resolveTokenOrFail() {
|
|
37
|
+
return resolveToken().catch((err) => fail('Authentication required:', err instanceof Error ? err.message : String(err)));
|
|
30
38
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
/** Build a client for a workflow resource, authenticated with `token`. */
|
|
40
|
+
export function clientFor(resource, token) {
|
|
41
|
+
const apiHost = resolveApiHost();
|
|
42
|
+
const baseParams = { token, apiVersion: API_VERSION, useCdn: false, ...(apiHost ? { apiHost } : {}) };
|
|
43
|
+
// A dataset goes through the classic {projectId, dataset} config — the proven
|
|
44
|
+
// path, and it keeps `tail`'s client.listen() on well-trodden ground. Other
|
|
45
|
+
// resource types have no projectId/dataset split, so they pass through as a
|
|
46
|
+
// resource directly.
|
|
47
|
+
if (resource.type === 'dataset') {
|
|
48
|
+
const { projectId, dataset } = datasetResourceParts(resource.id);
|
|
49
|
+
return createClient({ ...baseParams, projectId, dataset });
|
|
50
|
+
}
|
|
51
|
+
return createClient({ ...baseParams, resource });
|
|
40
52
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SanityClient } from '@sanity/client';
|
|
2
|
+
import type { WorkflowConfig, WorkflowDeployment, WorkflowInstance, WorkflowResource } from '@sanity/workflow-engine';
|
|
3
|
+
import type { EngineScope } from './operation-args.ts';
|
|
4
|
+
export interface DeploymentContext {
|
|
5
|
+
deployment: WorkflowDeployment;
|
|
6
|
+
client: SanityClient;
|
|
7
|
+
}
|
|
8
|
+
export interface InstanceContext {
|
|
9
|
+
client: SanityClient;
|
|
10
|
+
/** The engine scope derived from the instance itself — `{tag, workflowResource}`
|
|
11
|
+
* ready to pass straight to an engine verb, so callers never re-assemble it. */
|
|
12
|
+
scope: EngineScope;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The resolution a WRITE shares: discover the config, pick the deployment for
|
|
16
|
+
* `--tag` (or the sole one), and build an authenticated client for that
|
|
17
|
+
* deployment's resource. A write acts on one specific deployment, so a
|
|
18
|
+
* single, definite target is exactly what it needs.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveContext(flags: {
|
|
21
|
+
tag?: string | undefined;
|
|
22
|
+
}): Promise<DeploymentContext>;
|
|
23
|
+
/**
|
|
24
|
+
* The resolution a READ shares: an authenticated client for the resource to
|
|
25
|
+
* inspect — and nothing more. A read lists/inspects whatever is in the Content
|
|
26
|
+
* Lake, so it must NOT inherit a deployment's tag or definition set as a
|
|
27
|
+
* filter. The config only locates the resource here; the command applies the
|
|
28
|
+
* user's explicit `--tag` (if any) as a query filter itself.
|
|
29
|
+
*
|
|
30
|
+
* Resource selection: when every deployment targets the same resource (the
|
|
31
|
+
* common case) it's used with no `--tag` needed; only a config that spans
|
|
32
|
+
* *different* resources needs `--tag` to say which dataset to read.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveReadContext(flags: {
|
|
35
|
+
tag?: string | undefined;
|
|
36
|
+
}): Promise<{
|
|
37
|
+
client: SanityClient;
|
|
38
|
+
}>;
|
|
39
|
+
/** The single resource a read should target, or fail asking which when the
|
|
40
|
+
* config spans more than one. */
|
|
41
|
+
export declare function resolveReadResource(config: WorkflowConfig, tag: string | undefined): WorkflowResource;
|
|
42
|
+
/**
|
|
43
|
+
* The resolution an instance-id-targeted command shares, read or write: an
|
|
44
|
+
* authenticated client for the resource, plus the instance itself — so the
|
|
45
|
+
* command derives its partition (`tag`) from the instance, never from the
|
|
46
|
+
* config's declared deployments. An instance id is globally unique and carries
|
|
47
|
+
* its own `tag`, so a command that names one acts on that instance regardless
|
|
48
|
+
* of which tags the config happens to deploy; the config only locates the
|
|
49
|
+
* resource (via {@link resolveReadResource}, no tag filter).
|
|
50
|
+
*
|
|
51
|
+
* Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
|
|
52
|
+
* declared deployment because it acts on the authored definition set (or a
|
|
53
|
+
* definition name, which — unlike an instance id — isn't unique across tags).
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveInstanceContext(flags: {
|
|
56
|
+
tag?: string | undefined;
|
|
57
|
+
}, instanceId: string): Promise<InstanceContext>;
|
|
58
|
+
/** Fetch an instance by id, exiting cleanly when the resource has no such
|
|
59
|
+
* document — the diagnostic an operator sees on a mistyped id. Split out from
|
|
60
|
+
* {@link resolveInstanceContext} so the not-found path is unit-testable
|
|
61
|
+
* without a live config/token/client. */
|
|
62
|
+
export declare function loadInstanceOrFail(client: Pick<SanityClient, 'getDocument'>, instanceId: string): Promise<WorkflowInstance>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { clientFor, resolveTokenOrFail } from "./client.js";
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
import { loadWorkflowConfig } from "./load-config.js";
|
|
4
|
+
import { selectDeployment } from "./select-deployment.js";
|
|
5
|
+
/**
|
|
6
|
+
* The resolution a WRITE shares: discover the config, pick the deployment for
|
|
7
|
+
* `--tag` (or the sole one), and build an authenticated client for that
|
|
8
|
+
* deployment's resource. A write acts on one specific deployment, so a
|
|
9
|
+
* single, definite target is exactly what it needs.
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveContext(flags) {
|
|
12
|
+
const config = await loadWorkflowConfig();
|
|
13
|
+
const deployment = selectDeployment(config, { tag: flags.tag });
|
|
14
|
+
return { deployment, client: clientFor(deployment.workflowResource, await resolveTokenOrFail()) };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The resolution a READ shares: an authenticated client for the resource to
|
|
18
|
+
* inspect — and nothing more. A read lists/inspects whatever is in the Content
|
|
19
|
+
* Lake, so it must NOT inherit a deployment's tag or definition set as a
|
|
20
|
+
* filter. The config only locates the resource here; the command applies the
|
|
21
|
+
* user's explicit `--tag` (if any) as a query filter itself.
|
|
22
|
+
*
|
|
23
|
+
* Resource selection: when every deployment targets the same resource (the
|
|
24
|
+
* common case) it's used with no `--tag` needed; only a config that spans
|
|
25
|
+
* *different* resources needs `--tag` to say which dataset to read.
|
|
26
|
+
*/
|
|
27
|
+
export async function resolveReadContext(flags) {
|
|
28
|
+
const config = await loadWorkflowConfig();
|
|
29
|
+
return { client: clientFor(resolveReadResource(config, flags.tag), await resolveTokenOrFail()) };
|
|
30
|
+
}
|
|
31
|
+
/** The single resource a read should target, or fail asking which when the
|
|
32
|
+
* config spans more than one. */
|
|
33
|
+
export function resolveReadResource(config, tag) {
|
|
34
|
+
const byKey = new Map(config.deployments.map((d) => [
|
|
35
|
+
`${d.workflowResource.type}:${d.workflowResource.id}`,
|
|
36
|
+
d.workflowResource,
|
|
37
|
+
]));
|
|
38
|
+
const resources = [...byKey.values()];
|
|
39
|
+
const [sole] = resources;
|
|
40
|
+
if (sole === undefined) {
|
|
41
|
+
fail('No deployments configured.');
|
|
42
|
+
}
|
|
43
|
+
if (resources.length === 1) {
|
|
44
|
+
return sole;
|
|
45
|
+
}
|
|
46
|
+
if (tag === undefined) {
|
|
47
|
+
const tags = config.deployments.map((d) => d.tag).join(', ');
|
|
48
|
+
fail('Config spans multiple resources — pass --tag to choose one.', `Available tags: ${tags}`);
|
|
49
|
+
}
|
|
50
|
+
return selectDeployment(config, { tag }).workflowResource;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The resolution an instance-id-targeted command shares, read or write: an
|
|
54
|
+
* authenticated client for the resource, plus the instance itself — so the
|
|
55
|
+
* command derives its partition (`tag`) from the instance, never from the
|
|
56
|
+
* config's declared deployments. An instance id is globally unique and carries
|
|
57
|
+
* its own `tag`, so a command that names one acts on that instance regardless
|
|
58
|
+
* of which tags the config happens to deploy; the config only locates the
|
|
59
|
+
* resource (via {@link resolveReadResource}, no tag filter).
|
|
60
|
+
*
|
|
61
|
+
* Contrast {@link resolveContext}: the deploy/diff/delete path IS scoped to a
|
|
62
|
+
* declared deployment because it acts on the authored definition set (or a
|
|
63
|
+
* definition name, which — unlike an instance id — isn't unique across tags).
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveInstanceContext(flags, instanceId) {
|
|
66
|
+
const config = await loadWorkflowConfig();
|
|
67
|
+
const workflowResource = resolveReadResource(config, flags.tag);
|
|
68
|
+
const client = clientFor(workflowResource, await resolveTokenOrFail());
|
|
69
|
+
const instance = await loadInstanceOrFail(client, instanceId);
|
|
70
|
+
return { client, scope: { tag: instance.tag, workflowResource } };
|
|
71
|
+
}
|
|
72
|
+
/** Fetch an instance by id, exiting cleanly when the resource has no such
|
|
73
|
+
* document — the diagnostic an operator sees on a mistyped id. Split out from
|
|
74
|
+
* {@link resolveInstanceContext} so the not-found path is unit-testable
|
|
75
|
+
* without a live config/token/client. */
|
|
76
|
+
export async function loadInstanceOrFail(client, instanceId) {
|
|
77
|
+
const instance = await client.getDocument(instanceId);
|
|
78
|
+
if (!instance) {
|
|
79
|
+
fail(`Workflow instance ${instanceId} not found`);
|
|
80
|
+
}
|
|
81
|
+
return instance;
|
|
82
|
+
}
|
|
@@ -1,36 +1,45 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SanityClient } from '@sanity/client';
|
|
2
|
+
import { type DeployedDefinition, type WorkflowDefinition } from '@sanity/workflow-engine';
|
|
3
|
+
interface DefinitionShowQueryArgs {
|
|
4
|
+
name: string;
|
|
5
|
+
tag?: string | undefined;
|
|
6
|
+
version?: number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildDefinitionShowQuery(flags: DefinitionShowQueryArgs): {
|
|
9
|
+
groq: string;
|
|
10
|
+
params: Record<string, unknown>;
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* package such as `@sanity/workflow-examples`).
|
|
13
|
+
* Fetch the deployed definition a command targets — latest unless pinned via
|
|
14
|
+
* `version`. An absent pinned version always fails (the caller asked for
|
|
15
|
+
* something specific); an absent latest returns `undefined` so the caller
|
|
16
|
+
* decides whether that's fatal.
|
|
8
17
|
*/
|
|
9
|
-
export declare function
|
|
18
|
+
export declare function fetchDeployedDefinition({ client, name, tag, version, }: {
|
|
19
|
+
client: SanityClient;
|
|
20
|
+
name: string;
|
|
21
|
+
tag: string;
|
|
22
|
+
version: number | undefined;
|
|
23
|
+
}): Promise<DeployedDefinition | undefined>;
|
|
10
24
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
25
|
+
* Narrow a batch to one definition name (`only`), failing with the available
|
|
26
|
+
* names when there's no match. With `only` unset the whole batch passes through
|
|
27
|
+
* (a fresh array, so callers can mutate without aliasing). `context` prefixes
|
|
28
|
+
* the failure so callers whose run spans several deployments can attribute it.
|
|
15
29
|
*/
|
|
16
|
-
export declare function
|
|
30
|
+
export declare function selectDefinitions(allDefinitions: WorkflowDefinition[], { only, context }: {
|
|
31
|
+
only: string | undefined;
|
|
32
|
+
context?: string;
|
|
33
|
+
}): WorkflowDefinition[];
|
|
17
34
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*/
|
|
22
|
-
export declare function selectDefinitions(allDefinitions: WorkflowDefinition[], only: string | undefined): WorkflowDefinition[];
|
|
23
|
-
/**
|
|
24
|
-
* The full local-definitions pipeline shared by `deploy` and
|
|
25
|
-
* `definition diff`: resolve the `WORKFLOW_DEFS` source, import it,
|
|
26
|
-
* narrow to `only` (when given), and fail on any validation error.
|
|
27
|
-
*/
|
|
28
|
-
export declare function loadValidatedDefinitions(only: string | undefined): Promise<WorkflowDefinition[]>;
|
|
29
|
-
/** {@link loadValidatedDefinitions} narrowed to exactly one definition name. */
|
|
30
|
-
export declare function loadValidatedDefinition(name: string): Promise<WorkflowDefinition>;
|
|
31
|
-
/**
|
|
32
|
-
* Per-definition structural/GROQ checks plus one batch-level check the
|
|
33
|
-
* engine doesn't do for us: duplicate `name` within the local set.
|
|
34
|
-
* Returns the human-readable error lines; empty means the batch is valid.
|
|
35
|
+
* Per-definition structural/GROQ checks plus one batch-level check the engine
|
|
36
|
+
* doesn't do for us: duplicate `name` within the set. Returns the
|
|
37
|
+
* human-readable error lines; empty means the batch is valid.
|
|
35
38
|
*/
|
|
36
39
|
export declare function collectValidationErrors(defs: WorkflowDefinition[]): string[];
|
|
40
|
+
/** Validate a set of definitions, exiting with the collected errors on any
|
|
41
|
+
* failure. The single validate-then-fail gate `deploy` and `definition diff`
|
|
42
|
+
* share, so the two can't drift into divergent failure messages. `context`
|
|
43
|
+
* prefixes the failure with the deployment it belongs to in multi-target runs. */
|
|
44
|
+
export declare function validateOrFail(defs: WorkflowDefinition[], context?: string): void;
|
|
45
|
+
export {};
|