@sanity/runtime-cli 17.8.0 → 17.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 +26 -21
- package/dist/actions/blueprints/stacks.d.ts +2 -0
- package/dist/actions/blueprints/stacks.js +1 -0
- package/dist/actions/sanity/projects.d.ts +11 -0
- package/dist/actions/sanity/projects.js +48 -22
- package/dist/baseCommands.d.ts +6 -0
- package/dist/baseCommands.js +9 -1
- package/dist/commands/blueprints/stacks.d.ts +2 -1
- package/dist/commands/blueprints/stacks.js +14 -6
- package/dist/cores/blueprints/stacks.d.ts +8 -3
- package/dist/cores/blueprints/stacks.js +282 -85
- package/dist/cores/functions/test.d.ts +2 -19
- package/dist/cores/functions/test.js +25 -115
- package/dist/server/app.js +4 -6
- package/dist/server/static/components/payload-panel.js +1 -1
- package/dist/utils/display/prompt.d.ts +4 -1
- package/dist/utils/display/prompt.js +16 -2
- package/dist/utils/functions/child-runtime/durable.js +1 -1
- package/dist/utils/functions/invoke/local.js +14 -7
- package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +51 -0
- package/dist/utils/functions/invoke/resolve-invoke-type.js +131 -0
- package/dist/utils/types.d.ts +21 -2
- package/oclif.manifest.json +19 -5
- package/package.json +1 -1
- package/skills/blueprints-stacks.md +14 -2
|
@@ -1,39 +1,78 @@
|
|
|
1
1
|
import { listStacks } from '../../actions/blueprints/stacks.js';
|
|
2
|
-
import { listProjects } from '../../actions/sanity/projects.js';
|
|
2
|
+
import { listOrganizations, listProjects, } from '../../actions/sanity/projects.js';
|
|
3
|
+
import { CI_SETUP_DOCS_URL } from '../../constants.js';
|
|
3
4
|
import { formatStacksListing } from '../../utils/display/blueprints-formatting.js';
|
|
4
5
|
import { capitalize, niceId, warn } from '../../utils/display/presenters.js';
|
|
6
|
+
import { promptForOrganization, promptForProjectInOrg, promptForScope, promptForScopeRecovery, } from '../../utils/display/prompt.js';
|
|
7
|
+
import { isInteractive } from '../../utils/interactive.js';
|
|
5
8
|
import { styleText } from '../../utils/style-text.js';
|
|
6
|
-
/** Max projects to query for stacks
|
|
9
|
+
/** Max projects to query for stacks within one organization */
|
|
7
10
|
const PROJECT_LIMIT = 50;
|
|
11
|
+
/** Max projects to query for stacks across every organization */
|
|
12
|
+
const ALL_PROJECTS_LIMIT = 200;
|
|
13
|
+
/** Stack listings in flight while querying many scopes */
|
|
14
|
+
const SCOPE_CONCURRENCY = 5;
|
|
8
15
|
export async function blueprintStacksCore(options) {
|
|
9
|
-
const { log, token,
|
|
10
|
-
const { 'include-projects': includeProjects = false } = flags;
|
|
11
|
-
if (includeProjects && scopeType === 'organization') {
|
|
12
|
-
return deepListStacks({ organizationId: scopeId, token, log, blueprintStackId });
|
|
13
|
-
}
|
|
16
|
+
const { bin, log, token, localStackId: blueprintStackId, flags } = options;
|
|
17
|
+
const { all = false, 'include-projects': includeProjects = false, json = false } = flags;
|
|
14
18
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
if (all)
|
|
20
|
+
return await listStacksEverywhere({ token, log, blueprintStackId });
|
|
21
|
+
let { scopeType, scopeId } = options;
|
|
22
|
+
let displayName;
|
|
23
|
+
let promptedForScope = false;
|
|
24
|
+
let hasAlternativeScope = false;
|
|
25
|
+
while (true) {
|
|
26
|
+
if (!scopeType || !scopeId) {
|
|
27
|
+
if (!isInteractive || json)
|
|
28
|
+
return missingScopeResult(bin);
|
|
29
|
+
const chosen = await chooseScope({ token, log });
|
|
30
|
+
scopeType = chosen.scopeType;
|
|
31
|
+
scopeId = chosen.scopeId;
|
|
32
|
+
displayName = chosen.displayName;
|
|
33
|
+
hasAlternativeScope = chosen.hasAlternativeScope;
|
|
34
|
+
promptedForScope = true;
|
|
22
35
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
log('
|
|
32
|
-
|
|
36
|
+
if (includeProjects && scopeType === 'organization') {
|
|
37
|
+
return await listStacksAcrossOrganization({
|
|
38
|
+
organizationId: scopeId,
|
|
39
|
+
token,
|
|
40
|
+
log,
|
|
41
|
+
blueprintStackId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const spinner = log.ora('Fetching Stacks...').start();
|
|
45
|
+
const result = await listStacks({ token, scopeType, scopeId }, log);
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
if (promptedForScope && hasAlternativeScope && result.response?.status === 403) {
|
|
48
|
+
spinner.stop().clear();
|
|
49
|
+
log(warn(`Your account can see ${scopeTargetLabel({ scopeType, scopeId, displayName })}, but cannot read its Blueprint Stacks.`));
|
|
50
|
+
const recovery = await promptForScopeRecovery();
|
|
51
|
+
if (recovery === 'retry') {
|
|
52
|
+
scopeType = undefined;
|
|
53
|
+
scopeId = undefined;
|
|
54
|
+
displayName = undefined;
|
|
55
|
+
hasAlternativeScope = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
spinner.fail('Failed to list Stacks');
|
|
61
|
+
}
|
|
62
|
+
return listStacksFailure(result.error, result.response?.status);
|
|
63
|
+
}
|
|
64
|
+
spinner.stop().clear();
|
|
65
|
+
if (result.stacks.length === 0) {
|
|
66
|
+
log('No stacks found');
|
|
67
|
+
return { success: true, json: { stacks: [], scopeType, scopeId } };
|
|
68
|
+
}
|
|
69
|
+
printStackGroups({
|
|
70
|
+
groups: [{ scopeType, scopeId, displayName, stacks: result.stacks }],
|
|
71
|
+
log,
|
|
72
|
+
blueprintStackId,
|
|
73
|
+
});
|
|
74
|
+
return { success: true, json: { stacks: result.stacks, scopeType, scopeId } };
|
|
33
75
|
}
|
|
34
|
-
log(`${styleText('bold', capitalize(scopeType))} ${niceId(scopeId)} ${styleText('bold', 'Stacks')}:\n`);
|
|
35
|
-
log(formatStacksListing(stacks, blueprintStackId));
|
|
36
|
-
return { success: true, json: { stacks, scopeType, scopeId } };
|
|
37
76
|
}
|
|
38
77
|
catch (error) {
|
|
39
78
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -41,71 +80,229 @@ export async function blueprintStacksCore(options) {
|
|
|
41
80
|
return { success: false, error: errorMessage };
|
|
42
81
|
}
|
|
43
82
|
}
|
|
44
|
-
|
|
83
|
+
function listStacksFailure(error, status) {
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: error || 'Failed to list stacks',
|
|
87
|
+
suggestions: status === 403
|
|
88
|
+
? [
|
|
89
|
+
'Ask an administrator to grant you read access to Blueprint Stacks for this organization or project.',
|
|
90
|
+
'Try a different scope with --project-id or --organization-id.',
|
|
91
|
+
]
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function missingScopeResult(bin) {
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: 'Missing scope: provide --project-id or --organization-id.',
|
|
99
|
+
suggestions: [
|
|
100
|
+
`Run \`${bin} blueprints stacks --all\` to list Stacks for every organization and project.`,
|
|
101
|
+
'Set SANITY_PROJECT_ID or SANITY_ORGANIZATION_ID when no terminal is available.',
|
|
102
|
+
],
|
|
103
|
+
ref: CI_SETUP_DOCS_URL,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Organization, then project when the user picks project scope. */
|
|
107
|
+
async function chooseScope(options) {
|
|
108
|
+
const { token, log } = options;
|
|
109
|
+
const orgResult = await promptForOrganization({ token, logger: log });
|
|
110
|
+
if (typeof orgResult === 'symbol')
|
|
111
|
+
throw new Error('Unexpected back navigation');
|
|
112
|
+
const scopeType = await promptForScope({
|
|
113
|
+
hasProjects: orgResult.projects.length > 0,
|
|
114
|
+
message: 'Which Stacks would you like to list?',
|
|
115
|
+
});
|
|
116
|
+
if (typeof scopeType === 'symbol')
|
|
117
|
+
throw new Error('Unexpected back navigation');
|
|
118
|
+
if (scopeType === 'organization') {
|
|
119
|
+
const { id, name } = orgResult.organization;
|
|
120
|
+
return {
|
|
121
|
+
scopeType,
|
|
122
|
+
scopeId: id,
|
|
123
|
+
displayName: name,
|
|
124
|
+
hasAlternativeScope: orgResult.wasPrompted || orgResult.projects.length > 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const project = await promptForProjectInOrg({ projects: orgResult.projects, logger: log });
|
|
128
|
+
if (typeof project === 'symbol')
|
|
129
|
+
throw new Error('Unexpected back navigation');
|
|
130
|
+
return {
|
|
131
|
+
scopeType,
|
|
132
|
+
scopeId: project.projectId,
|
|
133
|
+
displayName: project.displayName,
|
|
134
|
+
// The containing organization is another possible scope even when it has no other projects.
|
|
135
|
+
hasAlternativeScope: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function listStacksAcrossOrganization(options) {
|
|
45
139
|
const { organizationId, token, log, blueprintStackId } = options;
|
|
46
|
-
const allStacks = [];
|
|
47
140
|
const spinner = log.ora('Fetching Stacks...').start();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
141
|
+
const projectsResult = await listProjects({ token, organizationId, logger: log });
|
|
142
|
+
if (!projectsResult.ok) {
|
|
143
|
+
spinner.fail('Failed to list projects');
|
|
144
|
+
return { success: false, error: projectsResult.error || 'Failed to list projects' };
|
|
145
|
+
}
|
|
146
|
+
const projects = projectsResult.projects;
|
|
147
|
+
const targets = [
|
|
148
|
+
{ scopeType: 'organization', scopeId: organizationId },
|
|
149
|
+
...projectTargets(projects.slice(0, PROJECT_LIMIT)),
|
|
150
|
+
];
|
|
151
|
+
const { groups, failures, successfulScopes } = await collectStackGroups({
|
|
152
|
+
targets,
|
|
153
|
+
token,
|
|
154
|
+
log,
|
|
155
|
+
onProgress: (completed, total) => {
|
|
156
|
+
spinner.text = `Fetching Stacks... (${completed}/${total})`;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
if (successfulScopes === 0 && failures.length > 0) {
|
|
160
|
+
spinner.fail('Failed to list Stacks');
|
|
161
|
+
return listStacksFailure(failures[0].error, failures[0].status);
|
|
162
|
+
}
|
|
163
|
+
spinner.stop().clear();
|
|
164
|
+
const stacks = groups.flatMap((group) => group.stacks);
|
|
165
|
+
if (stacks.length > 0)
|
|
166
|
+
printStackGroups({ groups, log, blueprintStackId });
|
|
167
|
+
else
|
|
168
|
+
log('No stacks found');
|
|
169
|
+
const organizationFailure = failures.find(({ target }) => target.scopeType === 'organization');
|
|
170
|
+
if (organizationFailure) {
|
|
171
|
+
log(warn(`Could not read organization-scoped Stacks: ${organizationFailure.error}`));
|
|
172
|
+
}
|
|
173
|
+
const projectFailures = failures.filter(({ target }) => target.scopeType === 'project');
|
|
174
|
+
if (projectFailures.length > 0) {
|
|
175
|
+
const projectCount = targets.filter(({ scopeType }) => scopeType === 'project').length;
|
|
176
|
+
log(warn(`Could not read Stacks for ${projectFailures.length} of ${projectCount} projects.`));
|
|
177
|
+
for (const { target, error } of projectFailures) {
|
|
178
|
+
log.verbose(`Project ${target.scopeId}: ${error}`);
|
|
57
179
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
180
|
+
}
|
|
181
|
+
if (projects.length > PROJECT_LIMIT)
|
|
182
|
+
log(projectLimitWarning(projects.length, PROJECT_LIMIT));
|
|
183
|
+
return { success: true, json: { stacks, scopeType: 'organization', scopeId: organizationId } };
|
|
184
|
+
}
|
|
185
|
+
/** Every organization and project the token can reach, skipping the scopes it cannot read. */
|
|
186
|
+
async function listStacksEverywhere(options) {
|
|
187
|
+
const { token, log, blueprintStackId } = options;
|
|
188
|
+
const spinner = log.ora('Finding organizations and projects...').start();
|
|
189
|
+
const [orgsResult, projectsResult] = await Promise.all([
|
|
190
|
+
listOrganizations({ token, logger: log }),
|
|
191
|
+
listProjects({ token, logger: log }),
|
|
192
|
+
]);
|
|
193
|
+
if (!orgsResult.ok && !projectsResult.ok) {
|
|
194
|
+
spinner.fail('Failed to list organizations and projects');
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: orgsResult.error || projectsResult.error || 'Failed to list organizations',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const discoveryWarnings = [];
|
|
201
|
+
if (!orgsResult.ok) {
|
|
202
|
+
discoveryWarnings.push(`Could not list organizations: ${orgsResult.error || 'unknown error'}`);
|
|
203
|
+
}
|
|
204
|
+
if (!projectsResult.ok) {
|
|
205
|
+
discoveryWarnings.push(`Could not list projects: ${projectsResult.error || 'unknown error'}`);
|
|
206
|
+
}
|
|
207
|
+
const organizations = orgsResult.ok ? orgsResult.organizations : [];
|
|
208
|
+
const projects = projectsResult.ok ? projectsResult.projects : [];
|
|
209
|
+
const targets = buildScopeTargets(organizations, projects.slice(0, ALL_PROJECTS_LIMIT));
|
|
210
|
+
spinner.text = 'Fetching Stacks...';
|
|
211
|
+
const { groups, failures, successfulScopes } = await collectStackGroups({
|
|
212
|
+
targets,
|
|
213
|
+
token,
|
|
214
|
+
log,
|
|
215
|
+
onProgress: (completed, total) => {
|
|
216
|
+
spinner.text = `Fetching Stacks... (${completed}/${total})`;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
if (targets.length > 0 && successfulScopes === 0) {
|
|
220
|
+
spinner.fail('Failed to list Stacks');
|
|
221
|
+
return listStacksFailure(failures[0]?.error, failures[0]?.status);
|
|
222
|
+
}
|
|
223
|
+
spinner.stop().clear();
|
|
224
|
+
const stacks = groups.flatMap((group) => group.stacks);
|
|
225
|
+
if (stacks.length > 0)
|
|
226
|
+
printStackGroups({ groups, log, blueprintStackId });
|
|
227
|
+
else if (targets.length === 0)
|
|
228
|
+
log('No organizations or projects found');
|
|
229
|
+
else
|
|
230
|
+
log('No stacks found');
|
|
231
|
+
for (const message of discoveryWarnings)
|
|
232
|
+
log(warn(message));
|
|
233
|
+
if (projects.length > ALL_PROJECTS_LIMIT) {
|
|
234
|
+
log(projectLimitWarning(projects.length, ALL_PROJECTS_LIMIT));
|
|
235
|
+
}
|
|
236
|
+
if (failures.length > 0) {
|
|
237
|
+
log(warn(`Could not read Stacks for ${failures.length} of ${targets.length} scopes.`));
|
|
238
|
+
for (const { target, error } of failures) {
|
|
239
|
+
log.verbose(`${capitalize(target.scopeType)} ${target.scopeId}: ${error}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { success: true, json: { stacks } };
|
|
243
|
+
}
|
|
244
|
+
async function collectStackGroups(options) {
|
|
245
|
+
const { targets, token, log, onProgress } = options;
|
|
246
|
+
const groups = [];
|
|
247
|
+
const failures = [];
|
|
248
|
+
let completed = 0;
|
|
249
|
+
let successfulScopes = 0;
|
|
250
|
+
for (let start = 0; start < targets.length; start += SCOPE_CONCURRENCY) {
|
|
251
|
+
const batch = await Promise.all(targets.slice(start, start + SCOPE_CONCURRENCY).map(async (target) => {
|
|
252
|
+
const { scopeType, scopeId } = target;
|
|
253
|
+
const result = await listStacks({ token, scopeType, scopeId }, log);
|
|
254
|
+
completed += 1;
|
|
255
|
+
onProgress?.(completed, targets.length);
|
|
256
|
+
return { target, result };
|
|
257
|
+
}));
|
|
258
|
+
for (const { target, result } of batch) {
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
failures.push({
|
|
261
|
+
target,
|
|
262
|
+
error: result.error || 'Failed to list stacks',
|
|
263
|
+
status: result.response?.status,
|
|
72
264
|
});
|
|
73
|
-
allStacks.push(...result.stacks);
|
|
74
265
|
}
|
|
75
|
-
else
|
|
76
|
-
|
|
266
|
+
else {
|
|
267
|
+
successfulScopes += 1;
|
|
268
|
+
if (result.stacks.length > 0)
|
|
269
|
+
groups.push({ ...target, stacks: result.stacks });
|
|
77
270
|
}
|
|
78
271
|
}
|
|
79
|
-
spinner.stop().clear();
|
|
80
|
-
// 4. Print results
|
|
81
|
-
if (orgStacks.length > 0) {
|
|
82
|
-
log(`${styleText('bold', 'Organization')} ${niceId(organizationId)} ${styleText('bold', 'Stacks')}:\n`);
|
|
83
|
-
log(formatStacksListing(orgStacks, blueprintStackId));
|
|
84
|
-
if (projectGroups.length > 0)
|
|
85
|
-
log(''); // blank line between sections
|
|
86
|
-
}
|
|
87
|
-
for (const group of projectGroups) {
|
|
88
|
-
log(`${styleText('bold', 'Project')} "${group.displayName}" ${niceId(group.projectId)} ${styleText('bold', 'Stacks')}:`);
|
|
89
|
-
log(formatStacksListing(group.stacks, blueprintStackId));
|
|
90
|
-
log('');
|
|
91
|
-
}
|
|
92
|
-
if (totalProjects > PROJECT_LIMIT) {
|
|
93
|
-
log(warn(`Organization has ${totalProjects} projects, only checked the first ${PROJECT_LIMIT}.`));
|
|
94
|
-
}
|
|
95
|
-
if (orgStacks.length === 0 && allStacks.length === 0) {
|
|
96
|
-
log('No stacks found');
|
|
97
|
-
return { success: true };
|
|
98
|
-
}
|
|
99
|
-
allStacks.push(...orgStacks);
|
|
100
|
-
return {
|
|
101
|
-
success: true,
|
|
102
|
-
json: { stacks: allStacks, scopeType: 'organization', scopeId: organizationId },
|
|
103
|
-
};
|
|
104
272
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
273
|
+
return { groups, failures, successfulScopes };
|
|
274
|
+
}
|
|
275
|
+
function buildScopeTargets(organizations, projects) {
|
|
276
|
+
const organizationIds = new Set(organizations.map(({ id }) => id));
|
|
277
|
+
const targets = [];
|
|
278
|
+
for (const { id, name } of organizations) {
|
|
279
|
+
targets.push({ scopeType: 'organization', scopeId: id, displayName: name });
|
|
280
|
+
targets.push(...projectTargets(projects.filter((p) => p.organizationId === id)));
|
|
110
281
|
}
|
|
282
|
+
targets.push(...projectTargets(projects.filter((p) => !organizationIds.has(p.organizationId))));
|
|
283
|
+
return targets;
|
|
284
|
+
}
|
|
285
|
+
function projectTargets(projects) {
|
|
286
|
+
return projects.map(({ id, displayName }) => ({
|
|
287
|
+
scopeType: 'project',
|
|
288
|
+
scopeId: id,
|
|
289
|
+
displayName,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
function scopeTargetLabel({ scopeType, scopeId, displayName }) {
|
|
293
|
+
const name = displayName ? ` "${displayName}"` : '';
|
|
294
|
+
return `${scopeType}${name} ${niceId(scopeId)}`;
|
|
295
|
+
}
|
|
296
|
+
function printStackGroups(options) {
|
|
297
|
+
const { groups, log, blueprintStackId } = options;
|
|
298
|
+
groups.forEach((group, index) => {
|
|
299
|
+
const name = group.displayName ? ` "${group.displayName}"` : '';
|
|
300
|
+
if (index > 0)
|
|
301
|
+
log('');
|
|
302
|
+
log(`${styleText('bold', capitalize(group.scopeType))}${name} ${niceId(group.scopeId)} ${styleText('bold', 'Stacks')}:\n`);
|
|
303
|
+
log(formatStacksListing(group.stacks, blueprintStackId));
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function projectLimitWarning(total, limit) {
|
|
307
|
+
return warn(`Found ${total} projects, only checked the first ${limit}.`);
|
|
111
308
|
}
|
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
import type { ReadBlueprintResult } from '../../actions/blueprints/blueprint.js';
|
|
2
|
+
import { type FunctionTestFlags } from '../../utils/types.js';
|
|
2
3
|
import type { CoreConfig, CoreResult } from '../index.js';
|
|
3
4
|
export interface FunctionTestOptions extends CoreConfig {
|
|
4
5
|
blueprint: ReadBlueprintResult;
|
|
5
6
|
args: {
|
|
6
7
|
name: string | undefined;
|
|
7
8
|
};
|
|
8
|
-
flags:
|
|
9
|
-
data?: string;
|
|
10
|
-
event?: string;
|
|
11
|
-
file?: string;
|
|
12
|
-
timeout?: number;
|
|
13
|
-
api?: string;
|
|
14
|
-
dataset?: string;
|
|
15
|
-
'project-id'?: string;
|
|
16
|
-
'organization-id'?: string;
|
|
17
|
-
'document-id'?: string;
|
|
18
|
-
'with-user-token'?: boolean;
|
|
19
|
-
'data-before'?: string;
|
|
20
|
-
'data-after'?: string;
|
|
21
|
-
'file-before'?: string;
|
|
22
|
-
'file-after'?: string;
|
|
23
|
-
'document-id-before'?: string;
|
|
24
|
-
'document-id-after'?: string;
|
|
25
|
-
'media-library-id'?: string;
|
|
26
|
-
};
|
|
9
|
+
flags: FunctionTestFlags;
|
|
27
10
|
error: (message: string, options: object) => void;
|
|
28
11
|
helpText: string;
|
|
29
12
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { cwd } from 'node:process';
|
|
1
|
+
import { dirname } from 'node:path';
|
|
4
2
|
import { testAction } from '../../actions/functions/test.js';
|
|
5
3
|
import config from '../../config.js';
|
|
6
|
-
import {
|
|
4
|
+
import { STEP_PHASE_GLYPH } from '../../constants.js';
|
|
7
5
|
import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
|
|
8
|
-
import buildPayload from '../../utils/functions/build-payload.js';
|
|
9
|
-
import { fetchAsset, fetchDocument } from '../../utils/functions/fetch-document.js';
|
|
10
6
|
import { findFunctionInResources, getFunctionNames } from '../../utils/functions/find.js';
|
|
11
7
|
import { runFunction } from '../../utils/functions/invoke/nested.js';
|
|
12
|
-
import {
|
|
8
|
+
import { buildContext, buildInvokeOptions, collectInvokeInput, defaultInvocationDeps, resolveInvocationPlan, } from '../../utils/functions/invoke/resolve-invoke-type.js';
|
|
13
9
|
import { isEventType, } from '../../utils/types.js';
|
|
14
10
|
/**
|
|
15
11
|
* CLI lines for a durable run, in emission order; bare `start` events dropped.
|
|
@@ -28,7 +24,7 @@ function renderStepTimeline(events) {
|
|
|
28
24
|
export async function functionTestCore(options) {
|
|
29
25
|
const { blueprint, log, error, args, flags, helpText } = options;
|
|
30
26
|
const { name: fnName } = args;
|
|
31
|
-
const {
|
|
27
|
+
const { event, timeout, api, 'with-user-token': withUserToken } = flags;
|
|
32
28
|
const { 'project-id': projectId = blueprint?.projectId, 'organization-id': organizationId = blueprint?.organizationId, } = flags;
|
|
33
29
|
if (!fnName) {
|
|
34
30
|
const functionNames = getFunctionNames(blueprint.resources);
|
|
@@ -54,9 +50,6 @@ export async function functionTestCore(options) {
|
|
|
54
50
|
}
|
|
55
51
|
try {
|
|
56
52
|
const resource = findFunctionInResources(blueprint.resources, fnName); // throws if not found
|
|
57
|
-
const docFunction = resource.type === SANITY_FUNCTION_DOCUMENT;
|
|
58
|
-
const mediaFunction = resource.type === SANITY_FUNCTION_MEDIA_LIBRARY_ASSET;
|
|
59
|
-
const durableFunction = resource.type === SANITY_FUNCTION_DURABLE;
|
|
60
53
|
const resolvedResources = await resolveDeployedResources({
|
|
61
54
|
resources: blueprint.resources,
|
|
62
55
|
stackId: blueprint.stackId,
|
|
@@ -65,110 +58,27 @@ export async function functionTestCore(options) {
|
|
|
65
58
|
: undefined,
|
|
66
59
|
logger: log,
|
|
67
60
|
});
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
resources: resolvedResources,
|
|
85
|
-
blueprintFileDir,
|
|
86
|
-
}
|
|
87
|
-
: { clientOptions: {}, resources: resolvedResources, blueprintFileDir };
|
|
88
|
-
// If the user sets the flag to use the real token set it in our options
|
|
89
|
-
if (withUserToken) {
|
|
90
|
-
contextOptions.clientOptions.token = config.token || undefined;
|
|
91
|
-
}
|
|
92
|
-
let before = null;
|
|
93
|
-
let after = null;
|
|
94
|
-
if (dataBefore && dataAfter) {
|
|
95
|
-
before = parseJsonObject(dataBefore);
|
|
96
|
-
after = parseJsonObject(dataAfter);
|
|
97
|
-
}
|
|
98
|
-
else if (fileBefore && fileAfter) {
|
|
99
|
-
let fileContents = readFileSync(join(cwd(), fileBefore), 'utf8');
|
|
100
|
-
before = parseJsonObject(fileContents);
|
|
101
|
-
fileContents = readFileSync(join(cwd(), fileAfter), 'utf8');
|
|
102
|
-
after = parseJsonObject(fileContents);
|
|
103
|
-
}
|
|
104
|
-
else if (documentIdBefore && documentIdAfter && docFunction) {
|
|
105
|
-
before = await fetchDocument(documentIdBefore, {
|
|
106
|
-
projectId,
|
|
107
|
-
dataset,
|
|
108
|
-
apiVersion: api,
|
|
109
|
-
apiHost: config.apiUrl,
|
|
110
|
-
token: config.token || undefined,
|
|
111
|
-
}, log);
|
|
112
|
-
after = await fetchDocument(documentIdAfter, {
|
|
113
|
-
projectId,
|
|
114
|
-
dataset,
|
|
115
|
-
apiVersion: api,
|
|
116
|
-
apiHost: config.apiUrl,
|
|
117
|
-
token: config.token || undefined,
|
|
118
|
-
}, log);
|
|
119
|
-
}
|
|
120
|
-
else if (documentIdBefore && documentIdAfter) {
|
|
121
|
-
before = await fetchAsset(documentIdBefore, {
|
|
122
|
-
mediaLibraryId,
|
|
123
|
-
apiVersion: api,
|
|
124
|
-
apiHost: config.apiUrl,
|
|
125
|
-
token: config.token || undefined,
|
|
126
|
-
}, log);
|
|
127
|
-
after = await fetchAsset(documentIdAfter, {
|
|
128
|
-
mediaLibraryId,
|
|
129
|
-
apiVersion: api,
|
|
130
|
-
apiHost: config.apiUrl,
|
|
131
|
-
token: config.token || undefined,
|
|
132
|
-
}, log);
|
|
133
|
-
}
|
|
134
|
-
let payload;
|
|
135
|
-
if (after) {
|
|
136
|
-
payload = after;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
if (documentId && docFunction) {
|
|
140
|
-
payload = await fetchDocument(documentId, {
|
|
141
|
-
projectId,
|
|
142
|
-
dataset,
|
|
143
|
-
apiVersion: api,
|
|
144
|
-
apiHost: config.apiUrl,
|
|
145
|
-
token: config.token || undefined,
|
|
146
|
-
}, log);
|
|
147
|
-
}
|
|
148
|
-
else if (documentId) {
|
|
149
|
-
payload = await fetchAsset(documentId, {
|
|
150
|
-
mediaLibraryId,
|
|
151
|
-
apiVersion: api,
|
|
152
|
-
apiHost: config.apiUrl,
|
|
153
|
-
token: config.token || undefined,
|
|
154
|
-
}, log);
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
payload = buildPayload({ data, file });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
let invokeOptions;
|
|
161
|
-
if (docFunction || mediaFunction) {
|
|
162
|
-
invokeOptions = { event: eventType, payload, before, after };
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
invokeOptions = { event: durableFunction ? 'durable' : 'scheduled', payload: { data: payload } };
|
|
166
|
-
}
|
|
61
|
+
const input = {
|
|
62
|
+
flags,
|
|
63
|
+
projectId,
|
|
64
|
+
organizationId,
|
|
65
|
+
api,
|
|
66
|
+
apiHost: config.apiUrl,
|
|
67
|
+
token: config.token || undefined,
|
|
68
|
+
withUserToken,
|
|
69
|
+
resources: resolvedResources,
|
|
70
|
+
blueprintFileDir: dirname(blueprint.fileInfo.blueprintFilePath),
|
|
71
|
+
log,
|
|
72
|
+
};
|
|
73
|
+
const plan = resolveInvocationPlan(resource);
|
|
74
|
+
const context = buildContext(plan, input);
|
|
75
|
+
const invokeInput = await collectInvokeInput(plan, input, defaultInvocationDeps);
|
|
76
|
+
const invokeOptions = buildInvokeOptions(plan, invokeInput, eventType);
|
|
167
77
|
// Records each nested context.invoke() so they can be surfaced in order after the spinner resolves.
|
|
168
78
|
const nestedRuns = [];
|
|
169
79
|
let nestedOrder = 0;
|
|
170
80
|
const stepEvents = [];
|
|
171
|
-
const onStepEvent =
|
|
81
|
+
const onStepEvent = plan.isDurable
|
|
172
82
|
? (event) => {
|
|
173
83
|
stepEvents.push(event);
|
|
174
84
|
}
|
|
@@ -185,7 +95,7 @@ export async function functionTestCore(options) {
|
|
|
185
95
|
const runNested = async (nestedName, nestedPayload, ctx, depth, callStack) => {
|
|
186
96
|
const order = ++nestedOrder;
|
|
187
97
|
const nestedResource = findFunctionInResources(blueprint.resources, nestedName);
|
|
188
|
-
const result = await runFunction(nestedResource, { payload:
|
|
98
|
+
const result = await runFunction(nestedResource, { payload: nestedPayload, event: 'nested' }, { ...ctx, resources: resolvedResources }, { depth, callStack, onNestedInvoke: runNested });
|
|
189
99
|
nestedRuns.push({ order, name: nestedName, logs: result.logs, error: result.error });
|
|
190
100
|
return result;
|
|
191
101
|
};
|
|
@@ -204,7 +114,7 @@ export async function functionTestCore(options) {
|
|
|
204
114
|
}
|
|
205
115
|
};
|
|
206
116
|
const spinner = log.ora('Executing function...').start();
|
|
207
|
-
const { json: response, logs, error, } = await testAction(resource, invokeOptions,
|
|
117
|
+
const { json: response, logs, error, } = await testAction(resource, invokeOptions, context, {
|
|
208
118
|
timeout: timeout ? timeout : resource.timeout,
|
|
209
119
|
onNestedInvoke: runNested,
|
|
210
120
|
depth: 0,
|
|
@@ -221,7 +131,7 @@ export async function functionTestCore(options) {
|
|
|
221
131
|
logNestedRuns();
|
|
222
132
|
const errorMessage = error.stack || error.message || error.name;
|
|
223
133
|
const isTimeout = error.message?.includes('Timeout');
|
|
224
|
-
if (
|
|
134
|
+
if (plan.isDurable && stepEvents.length > 0) {
|
|
225
135
|
log('Steps:');
|
|
226
136
|
for (const line of renderStepTimeline(stepEvents)) {
|
|
227
137
|
log(line);
|
|
@@ -244,7 +154,7 @@ export async function functionTestCore(options) {
|
|
|
244
154
|
log('Response:');
|
|
245
155
|
log(JSON.stringify(response, null, 2));
|
|
246
156
|
}
|
|
247
|
-
if (
|
|
157
|
+
if (plan.isDurable && stepEvents.length > 0) {
|
|
248
158
|
log('Steps:');
|
|
249
159
|
for (const line of renderStepTimeline(stepEvents)) {
|
|
250
160
|
log(line);
|
package/dist/server/app.js
CHANGED
|
@@ -28,7 +28,7 @@ const app = (host, port, validateResources, executionOptions) => {
|
|
|
28
28
|
*/
|
|
29
29
|
const nestedInvoke = async (name, payload, ctx, depth, callStack) => {
|
|
30
30
|
const order = ++nestedOrder;
|
|
31
|
-
const result = await handleInvokeRequest(name,
|
|
31
|
+
const result = await handleInvokeRequest(name, payload, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
|
|
32
32
|
broadcast({
|
|
33
33
|
type: 'nested-invoke',
|
|
34
34
|
name,
|
|
@@ -372,13 +372,13 @@ function parseInvokeRequest(body, blueprintFileDir) {
|
|
|
372
372
|
}
|
|
373
373
|
const metadataEvent = metadata.event;
|
|
374
374
|
if (typeof metadataEvent !== 'string' ||
|
|
375
|
-
(!isEventType(metadataEvent) && metadataEvent !== '
|
|
375
|
+
(!isEventType(metadataEvent) && metadataEvent !== 'plain')) {
|
|
376
376
|
throw new Error('Request body is not valid, `metadata.event` field is not one of `create`, `update`, `delete`, or `scheduled`');
|
|
377
377
|
}
|
|
378
378
|
let before = null;
|
|
379
379
|
let after = null;
|
|
380
380
|
// Only GROQ-based events (create, update, delete) have before and after fields
|
|
381
|
-
if (metadataEvent !== '
|
|
381
|
+
if (metadataEvent !== 'plain') {
|
|
382
382
|
if (!('before' in metadata)) {
|
|
383
383
|
throw new Error('Request body is not valid, `metadata.before` field is missing');
|
|
384
384
|
}
|
|
@@ -418,9 +418,7 @@ function parseInvokeRequest(body, blueprintFileDir) {
|
|
|
418
418
|
},
|
|
419
419
|
event,
|
|
420
420
|
},
|
|
421
|
-
metadata: metadataEvent === '
|
|
422
|
-
? { event: metadataEvent }
|
|
423
|
-
: { event: metadataEvent, before, after },
|
|
421
|
+
metadata: metadataEvent === 'plain' ? { event: metadataEvent } : { event: metadataEvent, before, after },
|
|
424
422
|
};
|
|
425
423
|
}
|
|
426
424
|
export { app, buildApiUrl, parseDocumentUrl };
|