@sanity/runtime-cli 11.0.3 → 11.1.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 +30 -26
- package/dist/actions/blueprints/blueprint.d.ts +3 -3
- package/dist/actions/blueprints/resources.d.ts +2 -2
- package/dist/actions/blueprints/resources.js +3 -4
- package/dist/commands/blueprints/add.js +9 -1
- package/dist/commands/functions/add.js +9 -1
- package/dist/commands/functions/test.d.ts +1 -0
- package/dist/commands/functions/test.js +6 -0
- package/dist/cores/blueprints/doctor.d.ts +2 -2
- package/dist/cores/blueprints/index.d.ts +2 -0
- package/dist/cores/blueprints/index.js +1 -0
- package/dist/cores/functions/add.js +31 -9
- package/dist/cores/functions/index.d.ts +9 -3
- package/dist/cores/functions/test.d.ts +1 -0
- package/dist/cores/functions/test.js +37 -7
- package/dist/server/app.js +99 -3
- package/dist/server/static/api.js +48 -2
- package/dist/server/static/components/app.css +16 -1
- package/dist/server/static/components/fetch-button.js +14 -5
- package/dist/server/static/components/filter-api-version.js +14 -0
- package/dist/server/static/components/filter-document-id.js +26 -0
- package/dist/server/static/components/filter-with-token.js +21 -0
- package/dist/server/static/components/filters.js +47 -42
- package/dist/server/static/components/function-list.js +15 -5
- package/dist/server/static/components/run-panel.js +11 -2
- package/dist/server/static/index.html +3 -0
- package/dist/utils/display/blueprints-formatting.d.ts +1 -1
- package/dist/utils/display/blueprints-formatting.js +4 -3
- package/dist/utils/display/resources-formatting.d.ts +2 -2
- package/dist/utils/functions/fetch-document.d.ts +2 -0
- package/dist/utils/functions/fetch-document.js +15 -0
- package/dist/utils/invoke-local.d.ts +2 -2
- package/dist/utils/invoke-local.js +11 -5
- package/dist/utils/types.d.ts +41 -5
- package/dist/utils/types.js +8 -0
- package/dist/utils/validate/resource.js +55 -30
- package/oclif.manifest.json +24 -3
- package/package.json +2 -2
package/dist/server/app.js
CHANGED
|
@@ -14,10 +14,10 @@ const app = (port, executionOptions) => {
|
|
|
14
14
|
switch (true) {
|
|
15
15
|
case req.url === '/blueprint': {
|
|
16
16
|
try {
|
|
17
|
-
const { parsedBlueprint, projectId } = await readLocalBlueprint();
|
|
17
|
+
const { parsedBlueprint, projectId, organizationId } = await readLocalBlueprint();
|
|
18
18
|
res.setHeader('Content-Type', 'application/json');
|
|
19
19
|
res.writeHead(200);
|
|
20
|
-
res.end(JSON.stringify({ parsedBlueprint, projectId })); // Use blueprint directly
|
|
20
|
+
res.end(JSON.stringify({ parsedBlueprint, projectId, organizationId })); // Use blueprint directly
|
|
21
21
|
}
|
|
22
22
|
catch (error) {
|
|
23
23
|
res.writeHead(404);
|
|
@@ -137,6 +137,75 @@ const app = (port, executionOptions) => {
|
|
|
137
137
|
}
|
|
138
138
|
break;
|
|
139
139
|
}
|
|
140
|
+
case req.url === '/organizations': {
|
|
141
|
+
res.setHeader('Content-Type', 'application/json');
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(`${config.apiUrl}/v2021-06-07/organizations?includeImplicitMemberships=true&includeMembers=false&includeFeatures=false`, {
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${config.token}`,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const json = await response.json();
|
|
149
|
+
res.writeHead(200);
|
|
150
|
+
res.end(JSON.stringify(json));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
res.writeHead(200);
|
|
154
|
+
res.end(JSON.stringify([]));
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case req.url?.startsWith('/media-libraries'): {
|
|
159
|
+
const url = req.url || '';
|
|
160
|
+
const matches = url.match(/[?&]organization=([^&]+)/) || [];
|
|
161
|
+
const organizationId = matches ? matches[1] : null;
|
|
162
|
+
res.setHeader('Content-Type', 'application/json');
|
|
163
|
+
try {
|
|
164
|
+
const response = await fetch(`${config.apiUrl}/vX/media-libraries?organizationId=${organizationId}`, {
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: `Bearer ${config.token}`,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const json = await response.json();
|
|
170
|
+
res.writeHead(200);
|
|
171
|
+
res.end(JSON.stringify(json.data));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
res.writeHead(200);
|
|
175
|
+
res.end(JSON.stringify([]));
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case req.url?.startsWith('/asset'): {
|
|
180
|
+
const url = req.url || '';
|
|
181
|
+
const parsed = parseAssetUrl(url);
|
|
182
|
+
if (parsed) {
|
|
183
|
+
const { mediaLibraryId, docId } = parsed;
|
|
184
|
+
res.setHeader('Content-Type', 'application/json');
|
|
185
|
+
try {
|
|
186
|
+
let json = {};
|
|
187
|
+
const url = buildAssetUrl(mediaLibraryId, docId, config.apiUrl);
|
|
188
|
+
if (docId) {
|
|
189
|
+
const response = await fetch(url, {
|
|
190
|
+
headers: {
|
|
191
|
+
Authorization: `Bearer ${config.token}`,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
const queryResponse = await response.json();
|
|
195
|
+
if (queryResponse?.documents[0]) {
|
|
196
|
+
json = queryResponse?.documents[0];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
res.writeHead(200);
|
|
200
|
+
res.end(JSON.stringify(json));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
res.writeHead(200);
|
|
204
|
+
res.end(JSON.stringify([]));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
140
209
|
default: {
|
|
141
210
|
const requestPath = req.url?.endsWith('/') ? `${req.url}index.html` : req.url;
|
|
142
211
|
const filePath = new URL(`./static${requestPath}`, import.meta.url).pathname;
|
|
@@ -178,6 +247,18 @@ function parseDocumentUrl(url) {
|
|
|
178
247
|
}
|
|
179
248
|
return null;
|
|
180
249
|
}
|
|
250
|
+
// Helper function to test URL parsing and document fetching logic
|
|
251
|
+
function parseAssetUrl(url) {
|
|
252
|
+
const matches = url.match(/[?&]organization=([^&]+).*?[&]medialibrary=([^&]+).*?[&]doc=([^&]+)/) || [];
|
|
253
|
+
if (matches && matches.length === 4) {
|
|
254
|
+
const [, organizationId, mediaLibraryId, docId] = matches;
|
|
255
|
+
// Ensure all parameters are present and non-empty
|
|
256
|
+
if (organizationId && mediaLibraryId && docId) {
|
|
257
|
+
return { organizationId, mediaLibraryId, docId };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
181
262
|
// Helper function to build expected API URL
|
|
182
263
|
function buildApiUrl(projectId, dataset, docId, apiUrl) {
|
|
183
264
|
const encodedQuery = encodeURIComponent(`*[_id == "${docId}"]`);
|
|
@@ -186,6 +267,10 @@ function buildApiUrl(projectId, dataset, docId, apiUrl) {
|
|
|
186
267
|
: `https://${projectId}.api.sanity.work/`;
|
|
187
268
|
return `${baseUrl}v1/data/query/${dataset}?query=${encodedQuery}`;
|
|
188
269
|
}
|
|
270
|
+
// Helper function to build expected API URL
|
|
271
|
+
function buildAssetUrl(mediaLibraryId, docId, apiUrl) {
|
|
272
|
+
return `${apiUrl}/v2025-03-24/media-libraries/${mediaLibraryId}/doc/${docId}`;
|
|
273
|
+
}
|
|
189
274
|
function parseInvokeRequest(body) {
|
|
190
275
|
let json;
|
|
191
276
|
try {
|
|
@@ -265,7 +350,18 @@ function parseInvokeRequest(body) {
|
|
|
265
350
|
};
|
|
266
351
|
return {
|
|
267
352
|
func,
|
|
268
|
-
data: {
|
|
353
|
+
data: {
|
|
354
|
+
context: {
|
|
355
|
+
...context,
|
|
356
|
+
clientOptions,
|
|
357
|
+
// Provide default values for required properties if not present
|
|
358
|
+
eventResourceType: typeof context.eventResourceType === 'string' ? context.eventResourceType : '',
|
|
359
|
+
eventResourceId: typeof context.eventResourceId === 'string' ? context.eventResourceId : '',
|
|
360
|
+
functionResourceType: typeof context.functionResourceType === 'string' ? context.functionResourceType : '',
|
|
361
|
+
functionResourceId: typeof context.functionResourceId === 'string' ? context.functionResourceId : '',
|
|
362
|
+
},
|
|
363
|
+
event,
|
|
364
|
+
},
|
|
269
365
|
metadata: { event: metadataEvent, before, after },
|
|
270
366
|
};
|
|
271
367
|
}
|
|
@@ -14,6 +14,9 @@ export default function API() {
|
|
|
14
14
|
invoke,
|
|
15
15
|
projects,
|
|
16
16
|
datasets,
|
|
17
|
+
organizations,
|
|
18
|
+
mediaLibraries,
|
|
19
|
+
asset,
|
|
17
20
|
store,
|
|
18
21
|
subscribe: store.subscribe,
|
|
19
22
|
unsubscribe: store.unsubscribe,
|
|
@@ -52,14 +55,16 @@ function blueprint() {
|
|
|
52
55
|
fetch('/blueprint')
|
|
53
56
|
.then((response) => response.json())
|
|
54
57
|
.then((blueprint) => {
|
|
55
|
-
const {parsedBlueprint, projectId} = blueprint
|
|
58
|
+
const {parsedBlueprint, projectId, organizationId} = blueprint
|
|
56
59
|
const functions = parsedBlueprint?.resources.filter((r) =>
|
|
57
60
|
r.type.startsWith('sanity.function.'),
|
|
58
61
|
)
|
|
59
62
|
|
|
60
63
|
store.functions = functions
|
|
61
64
|
store.selectedIndex = functions[0].name
|
|
65
|
+
store.selectedFunctionType = functions[0].type
|
|
62
66
|
store.defaultProject = projectId || undefined
|
|
67
|
+
store.defaultOrganization = organizationId || undefined
|
|
63
68
|
})
|
|
64
69
|
.catch(() => {
|
|
65
70
|
store.functions = []
|
|
@@ -69,9 +74,10 @@ function blueprint() {
|
|
|
69
74
|
function projects() {
|
|
70
75
|
fetch('/projects')
|
|
71
76
|
.then((response) => response.json())
|
|
72
|
-
.then((projects) => {
|
|
77
|
+
.then(async (projects) => {
|
|
73
78
|
store.projects = projects
|
|
74
79
|
store.selectedProject = store.defaultProject ? store.defaultProject : projects[0].id
|
|
80
|
+
await datasets(store.selectedProject)
|
|
75
81
|
})
|
|
76
82
|
.catch(() => {
|
|
77
83
|
store.projects = []
|
|
@@ -101,6 +107,46 @@ function document({projectId, dataset, docId}) {
|
|
|
101
107
|
})
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
function organizations() {
|
|
111
|
+
fetch('/organizations')
|
|
112
|
+
.then((response) => response.json())
|
|
113
|
+
.then(async (organizations) => {
|
|
114
|
+
store.organizations = organizations
|
|
115
|
+
store.selectedOrganization = store.defaultOrganization
|
|
116
|
+
? store.defaultOrganization
|
|
117
|
+
: organizations[0].id
|
|
118
|
+
await mediaLibraries(store.selectedOrganization)
|
|
119
|
+
})
|
|
120
|
+
.catch(() => {
|
|
121
|
+
store.organizations = []
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mediaLibraries(selectedOrganization) {
|
|
126
|
+
fetch(`/media-libraries?organization=${selectedOrganization}`)
|
|
127
|
+
.then((response) => response.json())
|
|
128
|
+
.then((mediaLibraries) => {
|
|
129
|
+
store.mediaLibraries = mediaLibraries
|
|
130
|
+
store.selectedMediaLibrary = store.defaultMediaLibrary
|
|
131
|
+
? store.defaultMediaLibrary
|
|
132
|
+
: mediaLibraries[0].id
|
|
133
|
+
})
|
|
134
|
+
.catch(() => {
|
|
135
|
+
store.mediaLibraries = []
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function asset({organizationId, mediaLibraryId, docId}) {
|
|
140
|
+
return fetch(`/asset?organization=${organizationId}&medialibrary=${mediaLibraryId}&doc=${docId}`)
|
|
141
|
+
.then((response) => response.json())
|
|
142
|
+
.then((doc) => {
|
|
143
|
+
store.document = doc
|
|
144
|
+
})
|
|
145
|
+
.catch(() => {
|
|
146
|
+
store.document = {}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
104
150
|
function getServerTimings(response) {
|
|
105
151
|
const timings = {}
|
|
106
152
|
const serverTiming = response.headers.get('Server-Timing')
|
|
@@ -678,9 +678,15 @@ body {
|
|
|
678
678
|
}
|
|
679
679
|
|
|
680
680
|
/* Utilities */
|
|
681
|
+
.capitalize {
|
|
682
|
+
text-transform: capitalize;
|
|
683
|
+
}
|
|
681
684
|
.flex {
|
|
682
685
|
display: flex;
|
|
683
686
|
}
|
|
687
|
+
.flex-column {
|
|
688
|
+
flex-direction: column;
|
|
689
|
+
}
|
|
684
690
|
.items-center {
|
|
685
691
|
align-items: center;
|
|
686
692
|
}
|
|
@@ -986,7 +992,6 @@ footer {
|
|
|
986
992
|
}
|
|
987
993
|
|
|
988
994
|
.function-list-item {
|
|
989
|
-
color: light-dark(var(--gray-800), var(--gray-400));
|
|
990
995
|
padding: 8px;
|
|
991
996
|
font-size: 0.875rem;
|
|
992
997
|
border-radius: 3px;
|
|
@@ -994,6 +999,16 @@ footer {
|
|
|
994
999
|
cursor: pointer;
|
|
995
1000
|
}
|
|
996
1001
|
|
|
1002
|
+
.function-list-item-label {
|
|
1003
|
+
color: light-dark(var(--gray-800), var(--gray-400));
|
|
1004
|
+
font-weight: var(--font-weight-3);
|
|
1005
|
+
padding-bottom: var(--space-1);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.function-list-item-type {
|
|
1009
|
+
color: light-dark(var(--gray-900), var(--gray-300));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
997
1012
|
.dropdown-select {
|
|
998
1013
|
background: transparent;
|
|
999
1014
|
color: light-dark(var(--gray-950), var(--gray-300));
|
|
@@ -63,11 +63,20 @@ export class FetchButton extends ApiBaseElement {
|
|
|
63
63
|
this.button.innerHTML = '<network-spinner></network-spinner>'
|
|
64
64
|
|
|
65
65
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// TODO: This assumes only two functions types for the sake of releasing today
|
|
67
|
+
if (this.api.store.selectedFunctionType === 'sanity.function.document') {
|
|
68
|
+
await this.api.document({
|
|
69
|
+
projectId: this.api.store.selectedProject,
|
|
70
|
+
dataset: this.api.store.selectedDataset,
|
|
71
|
+
docId,
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
await this.api.asset({
|
|
75
|
+
organizationId: this.api.store.selectedOrganization,
|
|
76
|
+
mediaLibraryId: this.api.store.selectedMediaLibrary,
|
|
77
|
+
docId,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
71
80
|
} catch (err) {
|
|
72
81
|
console.error('Error fetching document:', err)
|
|
73
82
|
} finally {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class ApiVersionComponent extends HTMLElement {
|
|
2
|
+
connectedCallback() {
|
|
3
|
+
this.innerHTML = `
|
|
4
|
+
<fieldset class="mar-t-sm">
|
|
5
|
+
<label class="slab-text">
|
|
6
|
+
<span style="display: block; margin-bottom: var(--space-1);">API Version</span>
|
|
7
|
+
<input name="apiVersion" id="apiversion" style="background: transparent; border-color: light-dark(var(--gray-200), var(--gray-700)); height: 2.5rem; color: light-dark(var(--gray-950), var(--gray-300));">
|
|
8
|
+
</label>
|
|
9
|
+
</fieldset>
|
|
10
|
+
`
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
customElements.define('api-version', ApiVersionComponent)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class DocumentIdFilter extends HTMLElement {
|
|
2
|
+
connectedCallback() {
|
|
3
|
+
this.innerHTML = `
|
|
4
|
+
<fieldset class="mar-t-sm" style="margin-top: 12px !important;">
|
|
5
|
+
<label class="slab-text">
|
|
6
|
+
<span style="display: block; margin-bottom: var(--space-1);">
|
|
7
|
+
<span style="display: flex; align-items: center;">
|
|
8
|
+
Document ID
|
|
9
|
+
<help-button>
|
|
10
|
+
<p>Fill out "Document ID" text field and then click the "Fetch Document" button to pre-populate the Document panel.</p>
|
|
11
|
+
<p>The Document panel is an editable text field so you can edit the fetched document or replace it with any JSON data you want.</p>
|
|
12
|
+
<p>When you click the "Run" button the contents of the Document panel will be evaluated against your filter/projection and sent to your function as the event part of the payload.</p>
|
|
13
|
+
</help-button>
|
|
14
|
+
</span>
|
|
15
|
+
</span>
|
|
16
|
+
<div style="display: flex; flex-direction: row; gap: var(--space-2);">
|
|
17
|
+
<input name="docid" id="docid" style="background: transparent; border-color: light-dark(var(--gray-200), var(--gray-700)); height: 2.5rem; color: light-dark(var(--gray-950), var(--gray-300));">
|
|
18
|
+
<fetch-button></fetch-button>
|
|
19
|
+
</div>
|
|
20
|
+
</label>
|
|
21
|
+
</fieldset>
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
customElements.define('document-id', DocumentIdFilter)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class WithTokenComponent extends HTMLElement {
|
|
2
|
+
connectedCallback() {
|
|
3
|
+
this.innerHTML = `
|
|
4
|
+
<fieldset class="mar-t-sm" style="display: flex; flex-direction: column; margin-top: 12px !important;">
|
|
5
|
+
<span class="slab-text" style="display: block; margin-bottom: var(--space-1);">
|
|
6
|
+
<span style="display: flex; align-items: center;">
|
|
7
|
+
With Token
|
|
8
|
+
<help-button>
|
|
9
|
+
<p><strong>With Token</strong> controls whether your function runs with or without authentication.</p>
|
|
10
|
+
<p><strong>Enabled:</strong> Function executes with your API token, providing full, authenticated access to your Sanity project.</p>
|
|
11
|
+
<p><strong>Disabled:</strong> Function executes without authentication, useful for testing public endpoints or functions that don't require user permissions.</p>
|
|
12
|
+
</help-button>
|
|
13
|
+
</span>
|
|
14
|
+
</span>
|
|
15
|
+
<toggle-switch toggle-key="withToken" style="display: flex; height: 2.5rem;"></toggle-switch>
|
|
16
|
+
</fieldset>
|
|
17
|
+
`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
customElements.define('with-token', WithTokenComponent)
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {ApiBaseElement} from './api-base.js'
|
|
2
|
+
|
|
3
|
+
class FiltersComponent extends ApiBaseElement {
|
|
4
|
+
renderFilters = () => {
|
|
5
|
+
const docFunction = this.api.store.selectedFunctionType === 'sanity.function.document'
|
|
6
|
+
const mediaFunction = this.api.store.selectedFunctionType?.startsWith(
|
|
7
|
+
'sanity.function.media-library',
|
|
8
|
+
)
|
|
9
|
+
|
|
3
10
|
this.innerHTML = `
|
|
4
11
|
<form style="gap: 8px; padding-left: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--card-border-color);">
|
|
5
12
|
<fieldset style="display: flex; gap: var(--space-2);">
|
|
6
13
|
<legend class="config-label">Client Options</legend>
|
|
7
|
-
|
|
14
|
+
${
|
|
15
|
+
docFunction
|
|
16
|
+
? `<select-dropdown
|
|
8
17
|
label="Project"
|
|
9
18
|
store-key="projects"
|
|
10
19
|
selected-key="selectedProject"
|
|
@@ -19,7 +28,30 @@ class FiltersComponent extends HTMLElement {
|
|
|
19
28
|
value-prop="name"
|
|
20
29
|
label-prop="name"
|
|
21
30
|
subscribe-to="selectedProject"
|
|
31
|
+
></select-dropdown>`
|
|
32
|
+
: ''
|
|
33
|
+
}
|
|
34
|
+
${
|
|
35
|
+
mediaFunction
|
|
36
|
+
? `<select-dropdown
|
|
37
|
+
label="Organization"
|
|
38
|
+
store-key="organizations"
|
|
39
|
+
selected-key="selectedOrganization"
|
|
40
|
+
value-prop="id"
|
|
41
|
+
label-prop="name"
|
|
42
|
+
trigger-fetch
|
|
22
43
|
></select-dropdown>
|
|
44
|
+
<select-dropdown
|
|
45
|
+
label="Media Library"
|
|
46
|
+
store-key="mediaLibraries"
|
|
47
|
+
selected-key="selectedMediaLibrary"
|
|
48
|
+
value-prop="id"
|
|
49
|
+
label-prop="id"
|
|
50
|
+
subscribe-to="selectedOrganization"
|
|
51
|
+
></select-dropdown>`
|
|
52
|
+
: ''
|
|
53
|
+
}
|
|
54
|
+
|
|
23
55
|
<select-dropdown
|
|
24
56
|
label="Event"
|
|
25
57
|
store-key="events"
|
|
@@ -27,49 +59,22 @@ class FiltersComponent extends HTMLElement {
|
|
|
27
59
|
value-prop="name"
|
|
28
60
|
label-prop="name"
|
|
29
61
|
></select-dropdown>
|
|
30
|
-
<fieldset class="mar-t-sm">
|
|
31
|
-
<label class="slab-text">
|
|
32
|
-
<span style="display: block; margin-bottom: var(--space-1);">API Version</span>
|
|
33
|
-
<input name="apiVersion" id="apiversion" style="background: transparent; border-color: light-dark(var(--gray-200), var(--gray-700)); height: 2.5rem; color: light-dark(var(--gray-950), var(--gray-300));">
|
|
34
|
-
</label>
|
|
35
|
-
</fieldset>
|
|
36
|
-
|
|
37
|
-
<fieldset class="mar-t-sm" style="display: flex; flex-direction: column; margin-top: 12px !important;">
|
|
38
|
-
<span class="slab-text" style="display: block; margin-bottom: var(--space-1);">
|
|
39
|
-
<span style="display: flex; align-items: center;">
|
|
40
|
-
With Token
|
|
41
|
-
<help-button>
|
|
42
|
-
<p><strong>With Token</strong> controls whether your function runs with or without authentication.</p>
|
|
43
|
-
<p><strong>Enabled:</strong> Function executes with your API token, providing full, authenticated access to your Sanity project.</p>
|
|
44
|
-
<p><strong>Disabled:</strong> Function executes without authentication, useful for testing public endpoints or functions that don't require user permissions.</p>
|
|
45
|
-
</help-button>
|
|
46
|
-
</span>
|
|
47
|
-
</span>
|
|
48
|
-
<toggle-switch toggle-key="withToken" style="display: flex; height: 2.5rem;"></toggle-switch>
|
|
49
|
-
</fieldset>
|
|
50
|
-
|
|
51
|
-
<fieldset class="mar-t-sm" style="margin-top: 12px !important;">
|
|
52
|
-
<label class="slab-text">
|
|
53
|
-
<span style="display: block; margin-bottom: var(--space-1);">
|
|
54
|
-
<span style="display: flex; align-items: center;">
|
|
55
|
-
Document ID
|
|
56
|
-
<help-button>
|
|
57
|
-
<p>Fill out "Document ID" text field and then click the "Fetch Document" button to pre-populate the Document panel.</p>
|
|
58
|
-
<p>The Document panel is an editable text field so you can edit the fetched document or replace it with any JSON data you want.</p>
|
|
59
|
-
<p>When you click the "Run" button the contents of the Document panel will be evaluated against your filter/projection and sent to your function as the event part of the payload.</p>
|
|
60
|
-
</help-button>
|
|
61
|
-
</span>
|
|
62
|
-
</span>
|
|
63
|
-
<div style="display: flex; flex-direction: row; gap: var(--space-2);">
|
|
64
|
-
<input name="docid" id="docid" style="background: transparent; border-color: light-dark(var(--gray-200), var(--gray-700)); height: 2.5rem; color: light-dark(var(--gray-950), var(--gray-300));">
|
|
65
|
-
<fetch-button></fetch-button>
|
|
66
|
-
</div>
|
|
67
|
-
</label>
|
|
68
|
-
</fieldset>
|
|
69
62
|
|
|
63
|
+
<api-version></api-version>
|
|
64
|
+
<with-token></with-token>
|
|
65
|
+
<document-id></document-id>
|
|
70
66
|
</form>
|
|
71
67
|
`
|
|
72
68
|
}
|
|
69
|
+
|
|
70
|
+
connectedCallback() {
|
|
71
|
+
this.innerHTML = this.renderFilters()
|
|
72
|
+
this.api.subscribe(this.renderFilters, ['selectedFunctionType'])
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
disconnectedCallback() {
|
|
76
|
+
this.api.unsubscribe(this.renderFilters)
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
customElements.define('filters-component', FiltersComponent)
|
|
@@ -7,19 +7,29 @@ const template = `<ol class="hidden-lg" type="content" style="padding:0 16px;"><
|
|
|
7
7
|
|
|
8
8
|
class FunctionList extends ApiBaseElement {
|
|
9
9
|
functionClicked = (event) => {
|
|
10
|
-
|
|
11
|
-
const target = this.api.store.functions.find((func) => func.name ===
|
|
10
|
+
const name = event.srcElement.closest('li').dataset.name
|
|
11
|
+
const target = this.api.store.functions.find((func) => func.name === name)
|
|
12
12
|
this.api.store.selectedIndex = target.name
|
|
13
|
+
this.api.store.selectedFunctionType = target.type
|
|
13
14
|
}
|
|
14
15
|
functionSelected = (event) => {
|
|
15
16
|
this.api.store.selectedIndex = event.srcElement.value
|
|
16
17
|
}
|
|
18
|
+
renderListItem = (func) => {
|
|
19
|
+
const selected = this.api.store.selectedIndex === func.name ? 'selected' : ''
|
|
20
|
+
return `<li data-name="${func.name}" class="function-list-item ${selected} flex flex-column" style="padding: 16px 24px;">
|
|
21
|
+
<span class="function-list-item-label">${func.name}</span>
|
|
22
|
+
<span class="function-list-item-type capitalize">${this.renderType(func.type)}</span>
|
|
23
|
+
</li>`
|
|
24
|
+
}
|
|
25
|
+
renderType = (type) => {
|
|
26
|
+
return type.split('.').pop().replaceAll('-', ' ')
|
|
27
|
+
}
|
|
17
28
|
renderFunctions = () => {
|
|
18
29
|
if (this.api.store.functions.length > 0) {
|
|
19
30
|
this.list.innerHTML = this.api.store.functions
|
|
20
31
|
.map((func) => {
|
|
21
|
-
|
|
22
|
-
return `<li class="function-list-item ${selected}" style="padding: 16px 24px;">${func.name}</li>`
|
|
32
|
+
return this.renderListItem(func)
|
|
23
33
|
})
|
|
24
34
|
.join('')
|
|
25
35
|
this.select.innerHTML = this.api.store.functions
|
|
@@ -29,7 +39,7 @@ class FunctionList extends ApiBaseElement {
|
|
|
29
39
|
})
|
|
30
40
|
.join('')
|
|
31
41
|
} else {
|
|
32
|
-
this.list.innerHTML = '<option class="pad-sm">No
|
|
42
|
+
this.list.innerHTML = '<option class="pad-sm">No Blueprint found</li>'
|
|
33
43
|
this.select.innerHTML = '<option>No blueprint.json file found</option>'
|
|
34
44
|
}
|
|
35
45
|
}
|
|
@@ -15,6 +15,8 @@ const template = `<div style="padding: var(--space-3);">
|
|
|
15
15
|
class RunPanel extends ApiBaseElement {
|
|
16
16
|
invoke = () => {
|
|
17
17
|
const selectedEvent = this.api.store.selectedEvent
|
|
18
|
+
const docFunction = this.api.store.selectedFunctionType === 'sanity.function.document'
|
|
19
|
+
|
|
18
20
|
this.api.store.result = {logs: '', time: 0}
|
|
19
21
|
let event = {}
|
|
20
22
|
let before = null
|
|
@@ -39,11 +41,19 @@ class RunPanel extends ApiBaseElement {
|
|
|
39
41
|
}
|
|
40
42
|
const context = {
|
|
41
43
|
clientOptions: {
|
|
42
|
-
apiVersion:
|
|
44
|
+
apiVersion: document.querySelector('#apiversion').value,
|
|
43
45
|
dataset: this.api.store.selectedDataset,
|
|
44
46
|
projectId: this.api.store.selectedProject,
|
|
45
47
|
token: this.api.store.withToken,
|
|
46
48
|
},
|
|
49
|
+
eventResourceType: docFunction ? 'dataset' : 'media-library',
|
|
50
|
+
eventResourceId: docFunction
|
|
51
|
+
? `${this.api.store.selectedProject}.${this.api.store.selectedDataset}`
|
|
52
|
+
: this.api.store.selectedMediaLibrary,
|
|
53
|
+
functionResourceType: 'project',
|
|
54
|
+
functionResourceId: docFunction
|
|
55
|
+
? this.api.store.selectedProject
|
|
56
|
+
: this.api.store.selectedOrganization,
|
|
47
57
|
}
|
|
48
58
|
const metadata = {
|
|
49
59
|
event: selectedEvent,
|
|
@@ -78,7 +88,6 @@ class RunPanel extends ApiBaseElement {
|
|
|
78
88
|
|
|
79
89
|
connectedCallback() {
|
|
80
90
|
this.innerHTML = template
|
|
81
|
-
this.apiVersion = document.querySelector('#apiversion')
|
|
82
91
|
this.datasetname = document.querySelector('#datasetname')
|
|
83
92
|
this.productId = document.querySelector('#project')
|
|
84
93
|
this.button = this.querySelector('button')
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
<footer><run-panel></run-panel></footer>
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
<script src="./components/filter-api-version.js" type="module"></script>
|
|
51
|
+
<script src="./components/filter-document-id.js" type="module"></script>
|
|
52
|
+
<script src="./components/filter-with-token.js" type="module"></script>
|
|
50
53
|
<script src="./components/filters.js" type="module"></script>
|
|
51
54
|
<script src="./components/select-dropdown.js" type="module"></script>
|
|
52
55
|
<script src="./components/function-list.js" type="module"></script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Blueprint } from '@sanity/blueprints-parser';
|
|
2
|
-
import type
|
|
2
|
+
import { type Resource, type Stack } from '../types.js';
|
|
3
3
|
export declare function formatTitle(title: string, name: string): string;
|
|
4
4
|
export declare function formatResourceTree(resources: Resource[] | undefined): string;
|
|
5
5
|
export declare function formatStackInfo(stack: Stack | Blueprint, isCurrentStack?: boolean): string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { treeify } from 'array-treeify';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { isLocalFunctionResource } from '../types.js';
|
|
3
4
|
import { formatDate, formatDuration } from './dates.js';
|
|
4
5
|
import { niceId } from './presenters.js';
|
|
5
6
|
import { arraifyCors, arrayifyFunction } from './resources-formatting.js';
|
|
@@ -13,7 +14,7 @@ export function formatResourceTree(resources) {
|
|
|
13
14
|
const RESOURCE_CATEGORIES = [
|
|
14
15
|
{
|
|
15
16
|
label: 'Functions',
|
|
16
|
-
match:
|
|
17
|
+
match: isLocalFunctionResource,
|
|
17
18
|
name: (res) => {
|
|
18
19
|
const name = chalk.bold.green(res.displayName || res.name || 'unnamed');
|
|
19
20
|
const ids = [
|
|
@@ -26,7 +27,7 @@ export function formatResourceTree(resources) {
|
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
label: 'CORS Origins',
|
|
29
|
-
match: (
|
|
30
|
+
match: (r) => r.type === 'sanity.project.cors',
|
|
30
31
|
name: (res) => {
|
|
31
32
|
const name = chalk.bold.yellow(res.displayName || res.name || 'unnamed');
|
|
32
33
|
const ids = [
|
|
@@ -42,7 +43,7 @@ export function formatResourceTree(resources) {
|
|
|
42
43
|
const matchedIndices = new Set();
|
|
43
44
|
for (const [i, resource] of resources.entries()) {
|
|
44
45
|
for (const cat of RESOURCE_CATEGORIES) {
|
|
45
|
-
if (cat.match(resource
|
|
46
|
+
if (cat.match(resource)) {
|
|
46
47
|
if (!categorized[cat.label])
|
|
47
48
|
categorized[cat.label] = [];
|
|
48
49
|
categorized[cat.label].push(resource);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { TreeInput } from 'array-treeify';
|
|
2
|
-
import type { CorsResource,
|
|
3
|
-
export declare function arrayifyFunction(fn:
|
|
2
|
+
import type { CorsResource, FunctionResourceBase } from '../../utils/types.js';
|
|
3
|
+
export declare function arrayifyFunction(fn: FunctionResourceBase): TreeInput;
|
|
4
4
|
export declare function arraifyCors(resource: CorsResource): TreeInput;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { type ClientConfig } from '@sanity/client';
|
|
2
|
+
import type { FetchConfig } from '../types.js';
|
|
2
3
|
export declare function fetchDocument(documentId: string, { projectId, dataset, useCdn, apiVersion, apiHost, token }: ClientConfig): Promise<Record<string, unknown>>;
|
|
4
|
+
export declare function fetchAsset(documentId: string, { mediaLibraryId, apiVersion, apiHost, token }: FetchConfig): Promise<Record<string, unknown>>;
|
|
@@ -10,3 +10,18 @@ export async function fetchDocument(documentId, { projectId, dataset, useCdn = t
|
|
|
10
10
|
}
|
|
11
11
|
return data[0];
|
|
12
12
|
}
|
|
13
|
+
export async function fetchAsset(documentId, { mediaLibraryId, apiVersion = '2025-03-24', apiHost, token }) {
|
|
14
|
+
const spinner = ora(`Fetching document ID ${documentId}...`).start();
|
|
15
|
+
const url = `${apiHost}/v${apiVersion}/media-libraries/${mediaLibraryId}/doc/${documentId}`;
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
spinner.stop();
|
|
23
|
+
if (!data?.documents[0]) {
|
|
24
|
+
throw Error(`Could not fetch document ID ${documentId}`);
|
|
25
|
+
}
|
|
26
|
+
return data.documents[0];
|
|
27
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { FunctionResource,
|
|
1
|
+
import type { FunctionResource, GroqRuleBase, InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadOptions } from './types.js';
|
|
2
2
|
export declare function sanitizeLogs(logs: string): string;
|
|
3
3
|
export declare const DEFAULT_GROQ_RULE: {
|
|
4
4
|
on: string[];
|
|
5
5
|
filter: string;
|
|
6
6
|
projection: string;
|
|
7
7
|
};
|
|
8
|
-
export declare function isDefaultGROQRule(rule:
|
|
8
|
+
export declare function isDefaultGROQRule(rule: GroqRuleBase | undefined): boolean;
|
|
9
9
|
export declare function applyGroqRule(resource: FunctionResource, data: Record<string, unknown> | null, before: Record<string, unknown> | null, after: Record<string, unknown> | null, projectId: string | undefined, dataset: string | undefined): Promise<any>;
|
|
10
10
|
export default function invoke(resource: FunctionResource, payload: InvokePayloadOptions, context: InvokeContextOptions, options: InvokeExecutionOptions): Promise<InvocationResponse>;
|