@sanity/runtime-cli 11.0.4 → 11.1.1

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.
Files changed (34) hide show
  1. package/README.md +30 -26
  2. package/dist/actions/blueprints/blueprint.d.ts +3 -3
  3. package/dist/actions/blueprints/resources.d.ts +2 -2
  4. package/dist/actions/blueprints/resources.js +3 -4
  5. package/dist/commands/blueprints/add.js +9 -1
  6. package/dist/commands/functions/add.js +9 -1
  7. package/dist/commands/functions/test.d.ts +1 -0
  8. package/dist/commands/functions/test.js +6 -0
  9. package/dist/cores/functions/add.js +31 -9
  10. package/dist/cores/functions/test.d.ts +1 -0
  11. package/dist/cores/functions/test.js +37 -7
  12. package/dist/server/app.js +99 -3
  13. package/dist/server/static/api.js +48 -2
  14. package/dist/server/static/components/app.css +16 -1
  15. package/dist/server/static/components/fetch-button.js +14 -5
  16. package/dist/server/static/components/filter-api-version.js +14 -0
  17. package/dist/server/static/components/filter-document-id.js +26 -0
  18. package/dist/server/static/components/filter-with-token.js +21 -0
  19. package/dist/server/static/components/filters.js +47 -42
  20. package/dist/server/static/components/function-list.js +15 -5
  21. package/dist/server/static/components/run-panel.js +11 -2
  22. package/dist/server/static/index.html +3 -0
  23. package/dist/utils/display/blueprints-formatting.d.ts +1 -1
  24. package/dist/utils/display/blueprints-formatting.js +4 -3
  25. package/dist/utils/display/resources-formatting.d.ts +2 -2
  26. package/dist/utils/functions/fetch-document.d.ts +2 -0
  27. package/dist/utils/functions/fetch-document.js +15 -0
  28. package/dist/utils/invoke-local.d.ts +2 -2
  29. package/dist/utils/invoke-local.js +11 -5
  30. package/dist/utils/types.d.ts +41 -5
  31. package/dist/utils/types.js +8 -0
  32. package/dist/utils/validate/resource.js +55 -30
  33. package/oclif.manifest.json +24 -3
  34. package/package.json +2 -2
@@ -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
- await this.api.document({
67
- projectId: this.api.store.selectedProject,
68
- dataset: this.api.store.selectedDataset,
69
- docId,
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
- class FiltersComponent extends HTMLElement {
2
- connectedCallback() {
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
- <select-dropdown
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
- // eslint-disable-next-line unicorn/prefer-dom-node-text-content
11
- const target = this.api.store.functions.find((func) => func.name === event.srcElement.innerText)
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
- const selected = this.api.store.selectedIndex === func.name ? 'selected' : ''
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 blueprint.json file found</li>'
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: this.apiVersion.value,
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 { Resource, Stack } from '../types.js';
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: (type) => type?.startsWith('sanity.function.'),
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: (type) => type === 'sanity.project.cors',
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.type)) {
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, FunctionResource } from '../../utils/types.js';
3
- export declare function arrayifyFunction(fn: FunctionResource): TreeInput;
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, GroqRule, InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadOptions } from './types.js';
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: GroqRule | undefined): boolean;
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>;
@@ -45,11 +45,17 @@ export async function applyGroqRule(resource, data, before, after, projectId, da
45
45
  const hasProjection = event.projection?.length;
46
46
  const projection = hasProjection ? `${event?.projection}` : '';
47
47
  const query = `*[${event?.filter}]${projection}`;
48
- const rule = groq.parse(query, { mode: 'delta' });
49
- const sanity = projectId && dataset ? { projectId, dataset } : undefined;
50
- const queryResults = await groq.evaluate(rule, { dataset: [data], before, after, sanity });
51
- const currentFunctionDocumentSet = await queryResults.get();
52
- return currentFunctionDocumentSet[0];
48
+ try {
49
+ const rule = groq.parse(query, { mode: 'delta' });
50
+ const sanity = projectId && dataset ? { projectId, dataset } : undefined;
51
+ const queryResults = await groq.evaluate(rule, { dataset: [data], before, after, sanity });
52
+ const currentFunctionDocumentSet = await queryResults.get();
53
+ return currentFunctionDocumentSet[0];
54
+ }
55
+ catch (groqErr) {
56
+ const detail = groqErr instanceof Error && `; ${groqErr.message}`;
57
+ throw new Error(`Invalid rule configuration: ${query}${detail}`, { cause: groqErr });
58
+ }
53
59
  }
54
60
  // default groq rule so just return the data
55
61
  return data;
@@ -15,17 +15,26 @@ export interface AuthParams {
15
15
  scopeId: string;
16
16
  }
17
17
  /** @internal */
18
- export interface GroqRule {
18
+ export type GroqRule = GroqRuleDocumentFunction | GroqRuleMediaLibraryFunction;
19
+ export interface GroqRuleBase {
19
20
  on: Array<string>;
20
21
  filter?: string;
22
+ projection?: string;
23
+ }
24
+ interface GroqRuleDocumentFunction extends GroqRuleBase {
21
25
  includeDrafts?: boolean;
22
26
  includeAllVersions?: boolean;
23
- projection?: string;
24
27
  resource?: {
25
28
  type: 'dataset';
26
29
  id: string;
27
30
  };
28
31
  }
32
+ interface GroqRuleMediaLibraryFunction extends GroqRuleBase {
33
+ resource: {
34
+ type: 'media-library';
35
+ id: string;
36
+ };
37
+ }
29
38
  /** @internal */
30
39
  export interface Resource {
31
40
  name: string;
@@ -38,16 +47,27 @@ export interface DeployedResource extends Resource {
38
47
  id: string;
39
48
  externalId: string;
40
49
  }
41
- export declare function isLocalFunctionResource(r: Resource): r is FunctionResource;
50
+ export declare function isLocalFunctionResource<T extends Resource>(r: T): r is T & FunctionResourceBase;
51
+ export declare function isDocumentFunctionResource<T extends Resource>(r: T): r is T & FunctionResourceDocument;
52
+ export declare function isMediaLibraryAssetFunctionResource<T extends Resource>(r: T): r is T & FunctionResourceMediaLibraryAsset;
42
53
  /** @internal */
43
- export interface FunctionResource extends Resource {
54
+ export type FunctionResource = FunctionResourceDocument | FunctionResourceMediaLibraryAsset | FunctionResourceBase;
55
+ export interface FunctionResourceBase extends Resource {
44
56
  src?: string;
45
57
  autoResolveDeps?: boolean;
46
58
  transpile?: boolean;
47
59
  memory?: number;
48
60
  timeout?: number;
49
61
  env?: Record<string, string>;
50
- event?: GroqRule;
62
+ event?: GroqRuleBase;
63
+ }
64
+ interface FunctionResourceDocument extends FunctionResourceBase {
65
+ type: 'sanity.function.document';
66
+ event?: GroqRuleDocumentFunction;
67
+ }
68
+ interface FunctionResourceMediaLibraryAsset extends FunctionResourceBase {
69
+ type: 'sanity.function.media-library.asset';
70
+ event: GroqRuleMediaLibraryFunction;
51
71
  }
52
72
  export interface CorsResource extends Resource {
53
73
  origin?: string;
@@ -102,6 +122,14 @@ export interface InvokeContextOptions {
102
122
  organizationId?: string;
103
123
  token?: string;
104
124
  };
125
+ /** The resource type of the event source; resource type that invoked the function. */
126
+ eventResourceType: string;
127
+ /** The resource ID of the event source; resource ID that invoked the function. */
128
+ eventResourceId: string;
129
+ /** The resource type of the function container; resource type that houses the function. */
130
+ functionResourceType: string;
131
+ /** The resource ID of the function container; resource ID that houses the function. */
132
+ functionResourceId: string;
105
133
  }
106
134
  /** @internal */
107
135
  export interface InvokeExecutionOptions {
@@ -152,3 +180,11 @@ export interface BlueprintParserError {
152
180
  message: string;
153
181
  type: string;
154
182
  }
183
+ /** @internal */
184
+ export declare interface FetchConfig {
185
+ mediaLibraryId?: string;
186
+ token?: string;
187
+ apiHost?: string;
188
+ apiVersion?: string;
189
+ }
190
+ export {};
@@ -1,6 +1,14 @@
1
1
  export function isLocalFunctionResource(r) {
2
2
  return r.type.startsWith('sanity.function.');
3
3
  }
4
+ // annoyed that checking for equality on `type` isnt sufficient to narrow down to a specific type from a union
5
+ // instead need to use these type predicated
6
+ export function isDocumentFunctionResource(r) {
7
+ return r.type === 'sanity.function.document';
8
+ }
9
+ export function isMediaLibraryAssetFunctionResource(r) {
10
+ return r.type === 'sanity.function.media-library.asset';
11
+ }
4
12
  export function isEventType(arg) {
5
13
  return ['create', 'update', 'delete'].includes(arg);
6
14
  }