@sanity/client 6.28.4-beta.0 → 6.28.4-resources.4

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.
@@ -333,6 +333,8 @@ export declare interface ClientConfig {
333
333
  /** @defaultValue true */
334
334
  useCdn?: boolean
335
335
  token?: string
336
+ /** @internal */
337
+ '~experimental_resource'?: ClientConfigResource
336
338
  /**
337
339
  * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}
338
340
  * @remarks
@@ -393,6 +395,24 @@ export declare interface ClientConfig {
393
395
  stega?: StegaConfig | boolean
394
396
  }
395
397
 
398
+ declare type ClientConfigResource =
399
+ | {
400
+ type: 'canvas'
401
+ id: string
402
+ }
403
+ | {
404
+ type: 'media-library'
405
+ id: string
406
+ }
407
+ | {
408
+ type: 'dataset'
409
+ id: string
410
+ }
411
+ | {
412
+ type: 'dashboard'
413
+ id: string
414
+ }
415
+
396
416
  /** @public */
397
417
  export declare class ClientError extends Error {
398
418
  response: ErrorProps['response']
@@ -333,6 +333,8 @@ export declare interface ClientConfig {
333
333
  /** @defaultValue true */
334
334
  useCdn?: boolean
335
335
  token?: string
336
+ /** @internal */
337
+ '~experimental_resource'?: ClientConfigResource
336
338
  /**
337
339
  * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}
338
340
  * @remarks
@@ -393,6 +395,24 @@ export declare interface ClientConfig {
393
395
  stega?: StegaConfig | boolean
394
396
  }
395
397
 
398
+ declare type ClientConfigResource =
399
+ | {
400
+ type: 'canvas'
401
+ id: string
402
+ }
403
+ | {
404
+ type: 'media-library'
405
+ id: string
406
+ }
407
+ | {
408
+ type: 'dataset'
409
+ id: string
410
+ }
411
+ | {
412
+ type: 'dashboard'
413
+ id: string
414
+ }
415
+
396
416
  /** @public */
397
417
  export declare class ClientError extends Error {
398
418
  response: ErrorProps['response']
package/dist/stega.d.cts CHANGED
@@ -333,6 +333,8 @@ export declare interface ClientConfig {
333
333
  /** @defaultValue true */
334
334
  useCdn?: boolean
335
335
  token?: string
336
+ /** @internal */
337
+ '~experimental_resource'?: ClientConfigResource
336
338
  /**
337
339
  * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}
338
340
  * @remarks
@@ -393,6 +395,24 @@ export declare interface ClientConfig {
393
395
  stega?: StegaConfig | boolean
394
396
  }
395
397
 
398
+ declare type ClientConfigResource =
399
+ | {
400
+ type: 'canvas'
401
+ id: string
402
+ }
403
+ | {
404
+ type: 'media-library'
405
+ id: string
406
+ }
407
+ | {
408
+ type: 'dataset'
409
+ id: string
410
+ }
411
+ | {
412
+ type: 'dashboard'
413
+ id: string
414
+ }
415
+
396
416
  /** @public */
397
417
  export declare class ClientError extends Error {
398
418
  response: ErrorProps['response']
package/dist/stega.d.ts CHANGED
@@ -333,6 +333,8 @@ export declare interface ClientConfig {
333
333
  /** @defaultValue true */
334
334
  useCdn?: boolean
335
335
  token?: string
336
+ /** @internal */
337
+ '~experimental_resource'?: ClientConfigResource
336
338
  /**
337
339
  * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}
338
340
  * @remarks
@@ -393,6 +395,24 @@ export declare interface ClientConfig {
393
395
  stega?: StegaConfig | boolean
394
396
  }
395
397
 
398
+ declare type ClientConfigResource =
399
+ | {
400
+ type: 'canvas'
401
+ id: string
402
+ }
403
+ | {
404
+ type: 'media-library'
405
+ id: string
406
+ }
407
+ | {
408
+ type: 'dataset'
409
+ id: string
410
+ }
411
+ | {
412
+ type: 'dashboard'
413
+ id: string
414
+ }
415
+
396
416
  /** @public */
397
417
  export declare class ClientError extends Error {
398
418
  response: ErrorProps['response']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/client",
3
- "version": "6.28.4-beta.0",
3
+ "version": "6.28.4-resources.4",
4
4
  "description": "Client for retrieving, creating and patching data from Sanity.io",
5
5
  "keywords": [
6
6
  "sanity",
@@ -125,39 +125,39 @@
125
125
  "devDependencies": {
126
126
  "@edge-runtime/types": "^4.0.0",
127
127
  "@edge-runtime/vm": "^5.0.0",
128
- "@eslint/eslintrc": "^3.2.0",
129
- "@eslint/js": "^9.20.0",
130
- "@rollup/plugin-commonjs": "^28.0.2",
131
- "@rollup/plugin-node-resolve": "^16.0.0",
128
+ "@eslint/eslintrc": "^3.3.0",
129
+ "@eslint/js": "^9.22.0",
130
+ "@rollup/plugin-commonjs": "^28.0.3",
131
+ "@rollup/plugin-node-resolve": "^16.0.1",
132
132
  "@sanity/client-latest": "npm:@sanity/client@latest",
133
- "@sanity/pkg-utils": "^7.0.4",
133
+ "@sanity/pkg-utils": "^7.1.1",
134
134
  "@types/json-diff": "^1.0.3",
135
135
  "@types/node": "^22.9.0",
136
- "@typescript-eslint/eslint-plugin": "^8.24.0",
137
- "@typescript-eslint/parser": "^8.24.0",
136
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
137
+ "@typescript-eslint/parser": "^8.26.1",
138
138
  "@vercel/stega": "0.1.2",
139
- "@vitest/coverage-v8": "3.0.5",
140
- "eslint": "^9.20.0",
141
- "eslint-config-prettier": "^10.0.1",
139
+ "@vitest/coverage-v8": "3.0.9",
140
+ "eslint": "^9.22.0",
141
+ "eslint-config-prettier": "^10.1.1",
142
142
  "eslint-formatter-compact": "^8.40.0",
143
143
  "eslint-plugin-prettier": "^5.2.3",
144
144
  "eslint-plugin-simple-import-sort": "^12.1.1",
145
145
  "faucet": "^0.0.4",
146
- "globals": "^15.14.0",
146
+ "globals": "^15.15.0",
147
147
  "happy-dom": "^12.10.3",
148
148
  "json-diff": "^1.0.6",
149
149
  "ls-engines": "^0.9.3",
150
- "msw": "^2.7.0",
151
- "next": "^15.1.7",
150
+ "msw": "^2.7.3",
151
+ "next": "^15.2.3",
152
152
  "nock": "^13.5.6",
153
- "prettier": "^3.5.0",
154
- "prettier-plugin-packagejson": "^2.5.8",
153
+ "prettier": "^3.5.3",
154
+ "prettier-plugin-packagejson": "^2.5.10",
155
155
  "rimraf": "^5.0.7",
156
- "rollup": "^4.34.6",
156
+ "rollup": "^4.36.0",
157
157
  "sse-channel": "^4.0.0",
158
- "terser": "^5.38.2",
159
- "typescript": "5.7.3",
160
- "vitest": "3.0.5"
158
+ "terser": "^5.39.0",
159
+ "typescript": "5.8.2",
160
+ "vitest": "3.0.9"
161
161
  },
162
162
  "packageManager": "npm@11.0.0",
163
163
  "engines": {
@@ -7,6 +7,7 @@ import type {
7
7
  Any,
8
8
  HttpRequest,
9
9
  HttpRequestEvent,
10
+ InitializedClientConfig,
10
11
  ResponseEvent,
11
12
  SanityAssetDocument,
12
13
  SanityImageAssetDocument,
@@ -149,8 +150,7 @@ function _upload(
149
150
  meta = ['none']
150
151
  }
151
152
 
152
- const dataset = validators.hasDataset(client.config())
153
- const assetEndpoint = assetType === 'image' ? 'images' : 'files'
153
+ const config = client.config()
154
154
  const options = optionsFromFile(opts, body)
155
155
  const {tag, label, title, description, creditLine, filename, source} = options
156
156
  const query: Any = {
@@ -166,17 +166,48 @@ function _upload(
166
166
  query.sourceName = source.name
167
167
  query.sourceUrl = source.url
168
168
  }
169
+
169
170
  return _requestObservable(client, httpRequest, {
170
171
  tag,
171
172
  method: 'POST',
172
173
  timeout: options.timeout || 0,
173
- uri: `/assets/${assetEndpoint}/${dataset}`,
174
+ uri: buildAssetUploadUrl(config, assetType),
174
175
  headers: options.contentType ? {'Content-Type': options.contentType} : {},
175
176
  query,
176
177
  body,
177
178
  })
178
179
  }
179
180
 
181
+ function buildAssetUploadUrl(config: InitializedClientConfig, assetType: 'image' | 'file'): string {
182
+ const assetTypeEndpoint = assetType === 'image' ? 'images' : 'files'
183
+
184
+ if (config['~experimental_resource']) {
185
+ const {type, id} = config['~experimental_resource']
186
+ switch (type) {
187
+ case 'dataset': {
188
+ throw new Error(
189
+ 'Assets are not supported for dataset resources, yet. Configure the client with `{projectId: <projectId>, dataset: <datasetId>}` instead.',
190
+ )
191
+ }
192
+ case 'canvas': {
193
+ return `/canvases/${id}/assets/${assetTypeEndpoint}`
194
+ }
195
+ case 'media-library': {
196
+ return `/media-libraries/${id}/upload`
197
+ }
198
+ case 'dashboard': {
199
+ return `/dashboards/${id}/assets/${assetTypeEndpoint}`
200
+ }
201
+ default:
202
+ // @ts-expect-error - handle all supported resource types
203
+ throw new Error(`Unsupported resource type: ${type.toString()}`)
204
+ }
205
+ }
206
+
207
+ const dataset = validators.hasDataset(config)
208
+ return `assets/${assetTypeEndpoint}/${dataset}`
209
+ }
210
+
180
211
  function optionsFromFile(opts: Record<string, Any>, file: Any) {
181
212
  if (typeof File === 'undefined' || !(file instanceof File)) {
182
213
  return opts
package/src/config.ts CHANGED
@@ -28,30 +28,15 @@ function validateApiVersion(apiVersion: string) {
28
28
  }
29
29
  }
30
30
 
31
- const VALID_PERSPECTIVE = /^[a-z0-9_]+$/i
32
-
33
31
  /**
34
32
  * @internal - it may have breaking changes in any release
35
33
  */
36
34
  export function validateApiPerspective(
37
35
  perspective: unknown,
38
36
  ): asserts perspective is ClientPerspective {
39
- if (Array.isArray(perspective)) {
40
- if (perspective.includes('raw')) {
41
- throw new TypeError(
42
- `Invalid API perspective value: "raw". The raw-perspective can not be combined with other perspectives`,
43
- )
44
- }
45
- }
46
-
47
- const invalid = (Array.isArray(perspective) ? perspective : [perspective]).filter(
48
- (perspectiveName) =>
49
- typeof perspectiveName !== 'string' || !VALID_PERSPECTIVE.test(perspectiveName),
50
- )
51
- if (invalid.length > 0) {
52
- const formatted = invalid.map((v) => JSON.stringify(v))
37
+ if (Array.isArray(perspective) && perspective.length > 1 && perspective.includes('raw')) {
53
38
  throw new TypeError(
54
- `Invalid API perspective value${invalid.length === 1 ? '' : 's'}: ${formatted.join(', ')}, expected \`published\`, \`drafts\`, \`raw\` or a release identifier string`,
39
+ `Invalid API perspective value: "raw". The raw-perspective can not be combined with other perspectives`,
55
40
  )
56
41
  }
57
42
  }
@@ -78,7 +63,7 @@ export const initConfig = (
78
63
  ...defaultConfig,
79
64
  ...specifiedConfig,
80
65
  } as InitializedClientConfig
81
- const projectBased = newConfig.useProjectHostname
66
+ const projectBased = newConfig.useProjectHostname && !newConfig['~experimental_resource']
82
67
 
83
68
  if (typeof Promise === 'undefined') {
84
69
  const helpUrl = generateHelpUrl('js-client-promise-polyfill')
@@ -89,6 +74,10 @@ export const initConfig = (
89
74
  throw new Error('Configuration must contain `projectId`')
90
75
  }
91
76
 
77
+ if (newConfig['~experimental_resource']) {
78
+ validate.resourceConfig(newConfig)
79
+ }
80
+
92
81
  if (typeof newConfig.perspective !== 'undefined') {
93
82
  validateApiPerspective(newConfig.perspective)
94
83
  }
@@ -123,11 +112,9 @@ export const initConfig = (
123
112
  const isLocalhost = isBrowser && isLocal(window.location.hostname)
124
113
 
125
114
  const hasToken = Boolean(newConfig.token)
126
- let withCredentials = Boolean(newConfig.withCredentials)
127
-
128
- if (withCredentials && hasToken) {
115
+ if (newConfig.withCredentials && hasToken) {
129
116
  warnings.printCredentialedTokenWarning()
130
- withCredentials = false
117
+ newConfig.withCredentials = false
131
118
  }
132
119
 
133
120
  if (isBrowser && isLocalhost && hasToken && newConfig.ignoreBrowserTokenWarning !== true) {
@@ -154,12 +141,12 @@ export const initConfig = (
154
141
  newConfig.apiVersion = `${newConfig.apiVersion}`.replace(/^v/, '')
155
142
  newConfig.isDefaultApi = newConfig.apiHost === defaultConfig.apiHost
156
143
 
157
- if (newConfig.useCdn === true && withCredentials) {
144
+ if (newConfig.useCdn === true && newConfig.withCredentials) {
158
145
  warnings.printCdnAndWithCredentialsWarning()
159
146
  }
160
147
 
161
148
  // If `useCdn` is undefined, we treat it as `true`
162
- newConfig.useCdn = newConfig.useCdn !== false && !withCredentials
149
+ newConfig.useCdn = newConfig.useCdn !== false && !newConfig.withCredentials
163
150
 
164
151
  validateApiVersion(newConfig.apiVersion)
165
152
 
@@ -168,7 +155,7 @@ export const initConfig = (
168
155
  const host = hostParts[1]
169
156
  const cdnHost = newConfig.isDefaultApi ? defaultCdnHost : host
170
157
 
171
- if (newConfig.useProjectHostname) {
158
+ if (projectBased) {
172
159
  newConfig.url = `${protocol}://${newConfig.projectId}.${host}/v${newConfig.apiVersion}`
173
160
  newConfig.cdnUrl = `${protocol}://${newConfig.projectId}.${cdnHost}/v${newConfig.apiVersion}`
174
161
  } else {
@@ -17,6 +17,7 @@ import type {
17
17
  HttpRequest,
18
18
  HttpRequestEvent,
19
19
  IdentifiedSanityDocumentStub,
20
+ InitializedClientConfig,
20
21
  InitializedStegaConfig,
21
22
  MultipleActionResult,
22
23
  MultipleMutationResult,
@@ -367,6 +368,27 @@ export function _create<R extends Record<string, Any>>(
367
368
  return _dataRequest(client, httpRequest, 'mutate', {mutations: [mutation]}, opts)
368
369
  }
369
370
 
371
+ const hasDataConfig = (client: SanityClient | ObservableSanityClient) =>
372
+ (client.config().dataset !== undefined && client.config().projectId !== undefined) ||
373
+ client.config()['~experimental_resource'] !== undefined
374
+ const isQuery = (client: SanityClient | ObservableSanityClient, uri: string) =>
375
+ hasDataConfig(client) && uri.startsWith(_getDataUrl(client, 'query'))
376
+ const isMutate = (client: SanityClient | ObservableSanityClient, uri: string) =>
377
+ hasDataConfig(client) && uri.startsWith(_getDataUrl(client, 'mutate'))
378
+ const isDoc = (client: SanityClient | ObservableSanityClient, uri: string) =>
379
+ hasDataConfig(client) && uri.startsWith(_getDataUrl(client, 'doc', ''))
380
+ const isListener = (client: SanityClient | ObservableSanityClient, uri: string) =>
381
+ hasDataConfig(client) && uri.startsWith(_getDataUrl(client, 'listen'))
382
+ const isHistory = (client: SanityClient | ObservableSanityClient, uri: string) =>
383
+ hasDataConfig(client) && uri.startsWith(_getDataUrl(client, 'history', ''))
384
+ const isData = (client: SanityClient | ObservableSanityClient, uri: string) =>
385
+ uri.startsWith('/data/') ||
386
+ isQuery(client, uri) ||
387
+ isMutate(client, uri) ||
388
+ isDoc(client, uri) ||
389
+ isListener(client, uri) ||
390
+ isHistory(client, uri)
391
+
370
392
  /**
371
393
  * @internal
372
394
  */
@@ -382,7 +404,7 @@ export function _requestObservable<R>(
382
404
  // Only the /data endpoint is currently available through API-CDN.
383
405
  const canUseCdn =
384
406
  typeof options.canUseCdn === 'undefined'
385
- ? ['GET', 'HEAD'].indexOf(options.method || 'GET') >= 0 && uri.indexOf('/data/') === 0
407
+ ? ['GET', 'HEAD'].indexOf(options.method || 'GET') >= 0 && isData(client, uri)
386
408
  : options.canUseCdn
387
409
 
388
410
  let useCdn = (options.useCdn ?? config.useCdn) && canUseCdn
@@ -397,10 +419,7 @@ export function _requestObservable<R>(
397
419
  }
398
420
 
399
421
  // GROQ query-only parameters
400
- if (
401
- ['GET', 'HEAD', 'POST'].indexOf(options.method || 'GET') >= 0 &&
402
- uri.indexOf('/data/query/') === 0
403
- ) {
422
+ if (['GET', 'HEAD', 'POST'].indexOf(options.method || 'GET') >= 0 && isQuery(client, uri)) {
404
423
  const resultSourceMap = options.resultSourceMap ?? config.resultSourceMap
405
424
  if (resultSourceMap !== undefined && resultSourceMap !== false) {
406
425
  options.query = {resultSourceMap, ...options.query}
@@ -482,9 +501,15 @@ export function _getDataUrl(
482
501
  path?: string,
483
502
  ): string {
484
503
  const config = client.config()
504
+ if (config['~experimental_resource']) {
505
+ validators.resourceConfig(config)
506
+ const resourceBase = resourceDataBase(config)
507
+ const uri = path !== undefined ? `${operation}/${path}` : operation
508
+ return `${resourceBase}/${uri}`.replace(/\/($|\?)/, '$1')
509
+ }
485
510
  const catalog = validators.hasDataset(config)
486
511
  const baseUri = `/${operation}/${catalog}`
487
- const uri = path ? `${baseUri}/${path}` : baseUri
512
+ const uri = path !== undefined ? `${baseUri}/${path}` : baseUri
488
513
  return `/data${uri}`.replace(/\/($|\?)/, '$1')
489
514
  }
490
515
 
@@ -547,3 +572,32 @@ function _createAbortError(signal?: AbortSignal) {
547
572
 
548
573
  return error
549
574
  }
575
+
576
+ const resourceDataBase = (config: InitializedClientConfig): string => {
577
+ if (!config['~experimental_resource']) {
578
+ throw new Error('`resource` must be provided to perform resource queries')
579
+ }
580
+ const {type, id} = config['~experimental_resource']
581
+
582
+ switch (type) {
583
+ case 'dataset': {
584
+ const segments = id.split('.')
585
+ if (segments.length !== 2) {
586
+ throw new Error('Dataset ID must be in the format "project.dataset"')
587
+ }
588
+ return `/projects/${segments[0]}/datasets/${segments[1]}`
589
+ }
590
+ case 'canvas': {
591
+ return `/canvases/${id}`
592
+ }
593
+ case 'media-library': {
594
+ return `/media-libraries/${id}`
595
+ }
596
+ case 'dashboard': {
597
+ return `/dashboards/${id}`
598
+ }
599
+ default:
600
+ // @ts-expect-error - handle all supported resource types
601
+ throw new Error(`Unsupported resource type: ${type.toString()}`)
602
+ }
603
+ }
package/src/data/live.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  SyncTag,
13
13
  } from '../types'
14
14
  import {shareReplayLatest} from '../util/shareReplayLatest'
15
+ import * as validate from '../validators'
15
16
  import {_getDataUrl} from './dataMethods'
16
17
  import {connectEventSource} from './eventsource'
17
18
  import {eventSourcePolyfill} from './eventsourcePolyfill'
@@ -43,6 +44,7 @@ export class LiveClient {
43
44
  */
44
45
  tag?: string
45
46
  } = {}): Observable<LiveEvent> {
47
+ validate.resourceGuard('live', this.#client.config())
46
48
  const {
47
49
  projectId,
48
50
  apiVersion: _apiVersion,
@@ -70,6 +70,7 @@ export class DatasetsClient {
70
70
  * @param options - Options for the dataset
71
71
  */
72
72
  create(name: string, options?: {aclMode?: DatasetAclMode}): Promise<DatasetResponse> {
73
+ validate.resourceGuard('dataset', this.#client.config())
73
74
  return lastValueFrom(
74
75
  _modify<DatasetResponse>(this.#client, this.#httpRequest, 'PUT', name, options),
75
76
  )
@@ -82,6 +83,7 @@ export class DatasetsClient {
82
83
  * @param options - New options for the dataset
83
84
  */
84
85
  edit(name: string, options?: {aclMode?: DatasetAclMode}): Promise<DatasetResponse> {
86
+ validate.resourceGuard('dataset', this.#client.config())
85
87
  return lastValueFrom(
86
88
  _modify<DatasetResponse>(this.#client, this.#httpRequest, 'PATCH', name, options),
87
89
  )
@@ -93,6 +95,7 @@ export class DatasetsClient {
93
95
  * @param name - Name of the dataset to delete
94
96
  */
95
97
  delete(name: string): Promise<{deleted: true}> {
98
+ validate.resourceGuard('dataset', this.#client.config())
96
99
  return lastValueFrom(_modify<{deleted: true}>(this.#client, this.#httpRequest, 'DELETE', name))
97
100
  }
98
101
 
@@ -100,6 +103,7 @@ export class DatasetsClient {
100
103
  * Fetch a list of datasets for the configured project
101
104
  */
102
105
  list(): Promise<DatasetsResponse> {
106
+ validate.resourceGuard('dataset', this.#client.config())
103
107
  return lastValueFrom(
104
108
  _request<DatasetsResponse>(this.#client, this.#httpRequest, {uri: '/datasets', tag: null}),
105
109
  )
@@ -113,6 +117,7 @@ function _modify<R = unknown>(
113
117
  name: string,
114
118
  options?: {aclMode?: DatasetAclMode},
115
119
  ) {
120
+ validate.resourceGuard('dataset', client.config())
116
121
  validate.dataset(name)
117
122
  return _request<R>(client, httpRequest, {
118
123
  method,
@@ -3,6 +3,7 @@ import {lastValueFrom, type Observable} from 'rxjs'
3
3
  import {_request} from '../data/dataMethods'
4
4
  import type {ObservableSanityClient, SanityClient} from '../SanityClient'
5
5
  import type {HttpRequest, SanityProject} from '../types'
6
+ import * as validate from '../validators'
6
7
 
7
8
  /** @internal */
8
9
  export class ObservableProjectsClient {
@@ -24,6 +25,7 @@ export class ObservableProjectsClient {
24
25
  list(options?: {
25
26
  includeMembers?: boolean
26
27
  }): Observable<SanityProject[] | Omit<SanityProject, 'members'>[]> {
28
+ validate.resourceGuard('projects', this.#client.config())
27
29
  const uri = options?.includeMembers === false ? '/projects?includeMembers=false' : '/projects'
28
30
  return _request<SanityProject[]>(this.#client, this.#httpRequest, {uri})
29
31
  }
@@ -34,6 +36,7 @@ export class ObservableProjectsClient {
34
36
  * @param projectId - ID of the project to fetch
35
37
  */
36
38
  getById(projectId: string): Observable<SanityProject> {
39
+ validate.resourceGuard('projects', this.#client.config())
37
40
  return _request<SanityProject>(this.#client, this.#httpRequest, {uri: `/projects/${projectId}`})
38
41
  }
39
42
  }
@@ -56,6 +59,7 @@ export class ProjectsClient {
56
59
  list(options?: {includeMembers?: true}): Promise<SanityProject[]>
57
60
  list(options?: {includeMembers?: false}): Promise<Omit<SanityProject, 'members'>[]>
58
61
  list(options?: {includeMembers?: boolean}): Promise<SanityProject[]> {
62
+ validate.resourceGuard('projects', this.#client.config())
59
63
  const uri = options?.includeMembers === false ? '/projects?includeMembers=false' : '/projects'
60
64
  return lastValueFrom(_request<SanityProject[]>(this.#client, this.#httpRequest, {uri}))
61
65
  }
@@ -66,6 +70,7 @@ export class ProjectsClient {
66
70
  * @param projectId - ID of the project to fetch
67
71
  */
68
72
  getById(projectId: string): Promise<SanityProject> {
73
+ validate.resourceGuard('projects', this.#client.config())
69
74
  return lastValueFrom(
70
75
  _request<SanityProject>(this.#client, this.#httpRequest, {uri: `/projects/${projectId}`}),
71
76
  )
package/src/types.ts CHANGED
@@ -53,6 +53,24 @@ export type ClientPerspective =
53
53
  | 'raw'
54
54
  | StackablePerspective[]
55
55
 
56
+ type ClientConfigResource =
57
+ | {
58
+ type: 'canvas'
59
+ id: string
60
+ }
61
+ | {
62
+ type: 'media-library'
63
+ id: string
64
+ }
65
+ | {
66
+ type: 'dataset'
67
+ id: string
68
+ }
69
+ | {
70
+ type: 'dashboard'
71
+ id: string
72
+ }
73
+
56
74
  /** @public */
57
75
  export interface ClientConfig {
58
76
  projectId?: string
@@ -61,6 +79,9 @@ export interface ClientConfig {
61
79
  useCdn?: boolean
62
80
  token?: string
63
81
 
82
+ /** @internal */
83
+ '~experimental_resource'?: ClientConfigResource
84
+
64
85
  /**
65
86
  * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}
66
87
  * @remarks
package/src/validators.ts CHANGED
@@ -76,3 +76,34 @@ export const requestTag = (tag: string) => {
76
76
 
77
77
  return tag
78
78
  }
79
+
80
+ export const resourceConfig = (config: InitializedClientConfig): void => {
81
+ if (!config['~experimental_resource']) {
82
+ throw new Error('`resource` must be provided to perform resource queries')
83
+ }
84
+ const {type, id} = config['~experimental_resource']
85
+
86
+ switch (type) {
87
+ case 'dataset': {
88
+ const segments = id.split('.')
89
+ if (segments.length !== 2) {
90
+ throw new Error('Dataset resource ID must be in the format "project.dataset"')
91
+ }
92
+ return
93
+ }
94
+ case 'dashboard':
95
+ case 'media-library':
96
+ case 'canvas': {
97
+ return
98
+ }
99
+ default:
100
+ // @ts-expect-error - handle all supported resource types
101
+ throw new Error(`Unsupported resource type: ${type.toString()}`)
102
+ }
103
+ }
104
+
105
+ export const resourceGuard = (service: string, config: InitializedClientConfig): void => {
106
+ if (config['~experimental_resource']) {
107
+ throw new Error(`\`${service}\` does not support resource-based operations`)
108
+ }
109
+ }