@sanity/client 7.26.2 → 8.0.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.
Files changed (130) hide show
  1. package/README.md +5 -377
  2. package/dist/browserUpload-CQgx9YYo.js +86 -0
  3. package/dist/browserUpload-CQgx9YYo.js.map +1 -0
  4. package/dist/browserUpload-icWlVP15.js +86 -0
  5. package/dist/browserUpload-icWlVP15.js.map +1 -0
  6. package/dist/config-a8VajuEY.js +126 -0
  7. package/dist/config-a8VajuEY.js.map +1 -0
  8. package/dist/csm.d.ts +122 -335
  9. package/dist/csm.js +79 -91
  10. package/dist/csm.js.map +1 -1
  11. package/dist/dist-C9ExSk2R.js +73 -0
  12. package/dist/dist-C9ExSk2R.js.map +1 -0
  13. package/dist/dist-Z8cIRxoB.js +69 -0
  14. package/dist/dist-Z8cIRxoB.js.map +1 -0
  15. package/dist/index.d.ts +107 -6723
  16. package/dist/index.js +2822 -2847
  17. package/dist/index.js.map +1 -1
  18. package/dist/{index.d.cts → index.node.d.ts} +4291 -5328
  19. package/dist/index.node.js +3131 -0
  20. package/dist/index.node.js.map +1 -0
  21. package/dist/isRecord-Kfmt-nk-.js +7 -0
  22. package/dist/isRecord-Kfmt-nk-.js.map +1 -0
  23. package/dist/media-library.d.ts +9 -100
  24. package/dist/media-library.js +36 -14
  25. package/dist/media-library.js.map +1 -1
  26. package/dist/request-CJxcN16k.js +372 -0
  27. package/dist/request-CJxcN16k.js.map +1 -0
  28. package/dist/request-k7VS_NnC.js +354 -0
  29. package/dist/request-k7VS_NnC.js.map +1 -0
  30. package/dist/resolveEditInfo-sq7yF78q.js +325 -0
  31. package/dist/resolveEditInfo-sq7yF78q.js.map +1 -0
  32. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  33. package/dist/stega.d.ts +78 -7137
  34. package/dist/stega.js +27 -20
  35. package/dist/stega.js.map +1 -1
  36. package/dist/stegaClean-C18wLWau.js +21 -0
  37. package/dist/stegaClean-C18wLWau.js.map +1 -0
  38. package/dist/stegaEncodeSourceMap-B2fGArSf.js +163 -0
  39. package/dist/stegaEncodeSourceMap-B2fGArSf.js.map +1 -0
  40. package/dist/stegaEncodeSourceMap-DkoIlutY.js +367 -0
  41. package/dist/stegaEncodeSourceMap-DkoIlutY.js.map +1 -0
  42. package/dist/{index.browser.d.cts → types-CUxZSgB2.d.ts} +3956 -5256
  43. package/dist/types-CfGzbXrl.d.ts +108 -0
  44. package/package.json +64 -141
  45. package/src/SanityClient.ts +82 -117
  46. package/src/agent/actions/AgentActionsClient.ts +12 -12
  47. package/src/agent/actions/commonTypes.ts +2 -1
  48. package/src/agent/actions/generate.ts +26 -6
  49. package/src/agent/actions/patch.ts +27 -7
  50. package/src/agent/actions/prompt.ts +19 -5
  51. package/src/agent/actions/transform.ts +23 -5
  52. package/src/agent/actions/translate.ts +23 -5
  53. package/src/assets/AssetsClient.ts +71 -23
  54. package/src/csm/applySourceDocuments.ts +3 -1
  55. package/src/csm/createEditUrl.ts +1 -1
  56. package/src/csm/draftUtils.ts +0 -1
  57. package/src/csm/index.ts +2 -2
  58. package/src/csm/studioPath.ts +2 -1
  59. package/src/csm/types.ts +8 -2
  60. package/src/data/dataMethods.ts +801 -349
  61. package/src/data/eventsource.ts +13 -10
  62. package/src/data/listen.ts +18 -23
  63. package/src/data/live.ts +44 -27
  64. package/src/data/resolveEventSourceFetch.ts +96 -0
  65. package/src/datasets/DatasetsClient.ts +58 -41
  66. package/src/defineCreateClient.ts +35 -23
  67. package/src/http/browserMiddleware.ts +7 -1
  68. package/src/http/browserUpload.ts +162 -0
  69. package/src/http/errors.ts +60 -6
  70. package/src/http/nodeMiddleware.ts +94 -30
  71. package/src/http/request.ts +310 -87
  72. package/src/http/requestOptions.ts +131 -15
  73. package/src/{index.browser.ts → index.node.ts} +9 -4
  74. package/src/index.ts +4 -3
  75. package/src/mediaLibrary/MediaLibraryVideoClient.ts +26 -11
  76. package/src/projects/ProjectsClient.ts +18 -16
  77. package/src/releases/ReleasesClient.ts +38 -33
  78. package/src/stega/index.ts +1 -2
  79. package/src/stega/stegaEncodeSourceMap.ts +5 -0
  80. package/src/stega/types.ts +2 -2
  81. package/src/types.ts +148 -105
  82. package/src/users/UsersClient.ts +10 -14
  83. package/src/util/shareReplayLatest.ts +1 -1
  84. package/src/warnings.ts +6 -1
  85. package/dist/_chunks-cjs/config.cjs +0 -206
  86. package/dist/_chunks-cjs/config.cjs.map +0 -1
  87. package/dist/_chunks-cjs/isRecord.cjs +0 -6
  88. package/dist/_chunks-cjs/isRecord.cjs.map +0 -1
  89. package/dist/_chunks-cjs/resolveEditInfo.cjs +0 -334
  90. package/dist/_chunks-cjs/resolveEditInfo.cjs.map +0 -1
  91. package/dist/_chunks-cjs/stegaClean.cjs +0 -46
  92. package/dist/_chunks-cjs/stegaClean.cjs.map +0 -1
  93. package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs +0 -388
  94. package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs.map +0 -1
  95. package/dist/_chunks-es/config.js +0 -207
  96. package/dist/_chunks-es/config.js.map +0 -1
  97. package/dist/_chunks-es/isRecord.js +0 -7
  98. package/dist/_chunks-es/isRecord.js.map +0 -1
  99. package/dist/_chunks-es/resolveEditInfo.js +0 -335
  100. package/dist/_chunks-es/resolveEditInfo.js.map +0 -1
  101. package/dist/_chunks-es/stegaClean.js +0 -47
  102. package/dist/_chunks-es/stegaClean.js.map +0 -1
  103. package/dist/_chunks-es/stegaEncodeSourceMap.js +0 -389
  104. package/dist/_chunks-es/stegaEncodeSourceMap.js.map +0 -1
  105. package/dist/csm.cjs +0 -99
  106. package/dist/csm.cjs.map +0 -1
  107. package/dist/csm.d.cts +0 -391
  108. package/dist/index.browser.cjs +0 -3106
  109. package/dist/index.browser.cjs.map +0 -1
  110. package/dist/index.browser.d.ts +0 -6802
  111. package/dist/index.browser.js +0 -3082
  112. package/dist/index.browser.js.map +0 -1
  113. package/dist/index.cjs +0 -2946
  114. package/dist/index.cjs.map +0 -1
  115. package/dist/media-library.cjs +0 -20
  116. package/dist/media-library.cjs.map +0 -1
  117. package/dist/media-library.d.cts +0 -117
  118. package/dist/stega.browser.cjs +0 -29
  119. package/dist/stega.browser.cjs.map +0 -1
  120. package/dist/stega.browser.d.cts +0 -7200
  121. package/dist/stega.browser.d.ts +0 -7200
  122. package/dist/stega.browser.js +0 -24
  123. package/dist/stega.browser.js.map +0 -1
  124. package/dist/stega.cjs +0 -29
  125. package/dist/stega.cjs.map +0 -1
  126. package/dist/stega.d.cts +0 -7200
  127. package/src/data/eventsourcePolyfill.ts +0 -7
  128. package/umd/.gitkeep +0 -1
  129. package/umd/sanityClient.js +0 -6543
  130. package/umd/sanityClient.min.js +0 -6
@@ -1,110 +1,333 @@
1
- import {getIt, type HttpContext, type Middlewares, type Requester} from 'get-it'
2
- import {jsonRequest, jsonResponse, observable, progress, retry} from 'get-it/middleware'
3
- import {Observable} from 'rxjs'
1
+ import {
2
+ createRequester,
3
+ type FetchFunction,
4
+ HttpError as GetItHttpError,
5
+ type RequestOptions as FetchRequestOptions,
6
+ type TransformMiddleware,
7
+ type WrappingMiddleware,
8
+ } from 'get-it'
9
+ import {isRetryableRequest, retry} from 'get-it/middleware'
10
+ import {from, Observable} from 'rxjs'
4
11
 
5
12
  import type {Any} from '../types'
6
- import {ClientError, ServerError} from './errors'
7
-
8
- const httpError = {
9
- onResponse: (res: Any, context: HttpContext) => {
10
- if (res.statusCode >= 500) {
11
- throw new ServerError(res)
12
- } else if (res.statusCode >= 400) {
13
- throw new ClientError(res, context)
14
- }
13
+ import {ClientError, httpResponseFromFetch, ServerError} from './errors'
14
+
15
+ /**
16
+ * Middleware accepted by the internal HTTP layer. Two flavors per get-it v9:
17
+ * a flat-pipeline transform (`beforeRequest`/`afterResponse`) or a wrapping
18
+ * middleware that surrounds the fetch chain.
19
+ *
20
+ * @internal
21
+ */
22
+ export type LegacyMiddleware = TransformMiddleware | WrappingMiddleware
15
23
 
16
- return res
17
- },
24
+ /**
25
+ * The shape this client's internal pipeline produces. Mirrors the historical
26
+ * `ResponseEvent` from the get-it v8 multi-event observable, so all the existing
27
+ * downstream code (`_requestObservable`, `_uploadObservable`,
28
+ * `defineCreateClient`) keeps working without churn.
29
+ *
30
+ * @internal
31
+ */
32
+ export interface ResponseEvent {
33
+ type: 'response'
34
+ body: unknown
35
+ statusCode: number
36
+ statusMessage: string | null
37
+ headers: Record<string, string>
38
+ url: string
39
+ method: string
18
40
  }
19
41
 
20
- function printWarnings(config: {ignoreWarnings?: string | RegExp | Array<string | RegExp>} = {}) {
21
- const seen: Record<string, boolean> = {}
42
+ /**
43
+ * Legacy "requester" type — the result of `defineHttpRequest`. Returns a
44
+ * single-event Observable for compatibility with the rest of the codebase.
45
+ *
46
+ * @internal
47
+ */
48
+ export type LegacyRequester = (options: Any) => Observable<ResponseEvent>
22
49
 
23
- // Helper function to check if a warning should be ignored
24
- const shouldIgnoreWarning = (message: string): boolean => {
25
- if (config.ignoreWarnings === undefined) return false
50
+ /**
51
+ * Promise-based sibling of {@link LegacyRequester}. Resolves directly to the
52
+ * single `ResponseEvent` the transport produces, skipping the RxJS wrapper.
53
+ * Used by the promise-based client surface so it never constructs an
54
+ * Observable.
55
+ *
56
+ * @internal
57
+ */
58
+ export type PromiseRequester = (options: Any) => Promise<ResponseEvent>
26
59
 
27
- const patterns = Array.isArray(config.ignoreWarnings)
28
- ? config.ignoreWarnings
29
- : [config.ignoreWarnings]
60
+ /**
61
+ * Both forms of the transport, sharing a single underlying get-it requester
62
+ * (so retry state and the one-shot warning de-duplication are shared between
63
+ * the observable and promise paths).
64
+ *
65
+ * @internal
66
+ */
67
+ export interface DualRequester {
68
+ observable: LegacyRequester
69
+ promise: PromiseRequester
70
+ }
71
+
72
+ /**
73
+ * Options for tuning the HTTP request pipeline per-client.
74
+ *
75
+ * @internal
76
+ */
77
+ export interface HttpRequestConfig {
78
+ ignoreWarnings?: string | RegExp | Array<string | RegExp>
79
+ maxRetries?: number
80
+ retryDelay?: (attemptNumber: number) => number
81
+ }
82
+
83
+ /**
84
+ * Build both the observable and promise transport forms from a single get-it
85
+ * requester. The promise form is the primitive (`executeRequest` is already
86
+ * promise-based); the observable form wraps it lazily so each subscription
87
+ * starts its own request (cold), and unsubscribing aborts the in-flight
88
+ * fetch — the same contract as the get-it v8 observable adapter.
89
+ *
90
+ * @internal
91
+ */
92
+ export function defineRequester(
93
+ envOptions: EnvironmentOptions,
94
+ config: HttpRequestConfig = {},
95
+ ): DualRequester {
96
+ // Framework-patched fetch implementations read extra `RequestInit` fields
97
+ // for caching semantics — Next.js App Router's `cache` and `next` options in
98
+ // particular. Legacy callers pass those via an object-valued `fetch` request
99
+ // option (see `adaptToFetchOptions`, which stashes it in `meta.fetchInit`
100
+ // since get-it v9's own `fetch` option only accepts a function). Merge them
101
+ // into the init of whichever fetch implementation is effective for the
102
+ // request: per-request/test-override fetch, the environment default, or the
103
+ // global fetch.
104
+ const applyFetchInit: WrappingMiddleware = (opts, next) => {
105
+ const fetchInit = opts.meta?.fetchInit
106
+ if (typeof fetchInit !== 'object' || fetchInit === null) return next(opts)
107
+ const baseFetch: NonNullable<FetchRequestOptions['fetch']> =
108
+ opts.fetch ?? envOptions.fetch ?? globalThis.fetch
109
+ const fetchWithInit: typeof baseFetch = (input, init) =>
110
+ baseFetch(input, {...fetchInit, ...init})
111
+ return next({...opts, fetch: fetchWithInit})
112
+ }
113
+
114
+ const requester = createRequester({
115
+ ...(envOptions.fetch ? {fetch: envOptions.fetch} : {}),
116
+ headers: envOptions.headers,
117
+ // Keep get-it's built-in 4xx/5xx → HttpError so the retry middleware can
118
+ // see them; we translate to ClientError/ServerError after the retry loop
119
+ // has exhausted in `executeRequest`.
120
+ httpErrors: true,
121
+ middleware: [
122
+ retry({
123
+ shouldRetry: shouldRetryRequest,
124
+ maxRetries: config.maxRetries ?? 5,
125
+ ...(config.retryDelay ? {retryDelay: config.retryDelay} : {}),
126
+ }),
127
+ ...envOptions.middleware,
128
+ applyFetchInit,
129
+ printWarnings(config),
130
+ ],
131
+ })
30
132
 
31
- return patterns.some((pattern) => {
32
- if (typeof pattern === 'string') {
33
- return message.includes(pattern)
34
- } else if (pattern instanceof RegExp) {
35
- return pattern.test(message)
133
+ const promise: PromiseRequester = (options: Any) => {
134
+ // Options arrive fetch-shaped from `requestOptions` — the single
135
+ // translation boundary between public option names and the transport.
136
+ if (typeof options.url !== 'string') {
137
+ throw new TypeError('Request options must include a `url`')
138
+ }
139
+ return executeRequest(requester, options)
140
+ }
141
+
142
+ // Same per-subscription AbortController pattern as `_observe` in
143
+ // dataMethods: a caller-supplied signal is combined in via
144
+ // `AbortSignal.any`, so the request aborts both on the caller's signal and
145
+ // on unsubscribe. `AbortSignal.any` (rather than `addEventListener`)
146
+ // because the caller's signal can be long-lived and reused — a manually
147
+ // added listener would accumulate there once per subscription, since
148
+ // `{once: true}` only cleans up if the signal actually fires.
149
+ const observable: LegacyRequester = (options: Any) =>
150
+ new Observable<ResponseEvent>((subscriber) => {
151
+ const controller = new AbortController()
152
+ const userSignal: AbortSignal | undefined = options.signal
153
+ const signal = userSignal
154
+ ? AbortSignal.any([userSignal, controller.signal])
155
+ : controller.signal
156
+ const subscription = from(promise({...options, signal})).subscribe(subscriber)
157
+ return () => {
158
+ subscription.unsubscribe()
159
+ controller.abort()
36
160
  }
37
- return false
38
161
  })
162
+
163
+ return {promise, observable}
164
+ }
165
+
166
+ /** @internal */
167
+ export function defineHttpRequest(
168
+ envOptions: EnvironmentOptions,
169
+ config: HttpRequestConfig = {},
170
+ ): LegacyRequester {
171
+ return defineRequester(envOptions, config).observable
172
+ }
173
+
174
+ /**
175
+ * Options describing the environment-specific defaults (Node vs. browser).
176
+ *
177
+ * @internal
178
+ */
179
+ export interface EnvironmentOptions {
180
+ fetch?: FetchRequestOptions['fetch']
181
+ headers?: Record<string, string>
182
+ middleware: LegacyMiddleware[]
183
+ /**
184
+ * Resolves the environment's fetch implementation — the same transport
185
+ * regular requests use (custom fetch variants, undici configuration,
186
+ * env-proxy support and all), optionally configured for an explicit proxy
187
+ * URL. Lets consumers of the resolved config (the EventSource fetch
188
+ * resolver) avoid falling back to whatever `globalThis.fetch` happens to
189
+ * be. The Node environment supplies get-it's undici-backed fetch; the
190
+ * browser environment leaves it unset (the global fetch IS the
191
+ * environment's fetch there).
192
+ *
193
+ * Looked up via the env rather than imported directly so that the Node-only
194
+ * `get-it/node` (which transitively pulls in `undici`) never ends up in the
195
+ * browser bundle, even via rollup's inlined dynamic imports.
196
+ */
197
+ resolveFetch?: (proxyUrl?: string) => FetchFunction
198
+ }
199
+
200
+ async function executeRequest(
201
+ requester: ReturnType<typeof createRequester>,
202
+ fetchOptions: FetchRequestOptions,
203
+ ): Promise<ResponseEvent> {
204
+ const url = fetchOptions.url
205
+ const method = (fetchOptions.method ?? 'GET').toUpperCase()
206
+
207
+ let response
208
+ try {
209
+ response = await requester(fetchOptions)
210
+ } catch (err) {
211
+ if (err instanceof GetItHttpError) {
212
+ // `err.body` is the response body as a string (get-it v9 stores the
213
+ // already-decoded text), regardless of which response variant
214
+ // `err.response` is.
215
+ const errBodyText = typeof err.body === 'string' ? err.body : ''
216
+ const errBody = parseJsonText(errBodyText, err.headers)
217
+ const canonical = httpResponseFromFetch(
218
+ {
219
+ status: err.status,
220
+ statusText: err.statusText,
221
+ headers: err.headers,
222
+ body: errBody,
223
+ },
224
+ url,
225
+ method,
226
+ )
227
+ const tag = extractRequestTag(fetchOptions.query)
228
+ if (canonical.statusCode >= 500) {
229
+ throw new ServerError(canonical)
230
+ }
231
+ throw new ClientError(canonical, tag)
232
+ }
233
+ throw err
39
234
  }
40
235
 
236
+ const body = parseJsonBody(response)
41
237
  return {
42
- onResponse: (res: Any) => {
43
- const warn = res.headers['x-sanity-warning']
44
- const warnings = Array.isArray(warn) ? warn : [warn]
45
- for (const msg of warnings) {
46
- if (!msg || seen[msg]) continue
238
+ type: 'response',
239
+ body,
240
+ statusCode: response.status,
241
+ statusMessage: response.statusText || null,
242
+ headers: headersToRecord(response.headers),
243
+ url,
244
+ method,
245
+ }
246
+ }
47
247
 
48
- // Skip warnings that match ignore patterns
49
- if (shouldIgnoreWarning(msg)) {
50
- continue
51
- }
248
+ /**
249
+ * Extract the GROQ request tag (used for error messages) from the query.
250
+ */
251
+ function extractRequestTag(query: FetchRequestOptions['query']): string | undefined {
252
+ if (!query) return undefined
253
+ if (query instanceof URLSearchParams) return query.get('tag') ?? undefined
254
+ const tag = query.tag
255
+ return typeof tag === 'string' ? tag : undefined
256
+ }
52
257
 
53
- seen[msg] = true
54
- console.warn(msg) // eslint-disable-line no-console
55
- }
56
- return res
57
- },
258
+ function parseJsonBody(response: {headers: Headers; text(): string}): unknown {
259
+ return parseJsonText(response.text(), response.headers)
260
+ }
261
+
262
+ /**
263
+ * Parse a response body according to its `content-type`: JSON when the header
264
+ * says so (falling back to the raw text on malformed JSON), text otherwise.
265
+ * Shared with the browser XHR upload path so error bodies parse identically
266
+ * on both transports.
267
+ *
268
+ * @internal
269
+ */
270
+ export function parseJsonText(text: string, headers: Headers): unknown {
271
+ const contentType = (headers.get('content-type') ?? '').toLowerCase()
272
+ if (!text) return undefined
273
+ if (contentType.includes('application/json')) {
274
+ try {
275
+ return JSON.parse(text)
276
+ } catch {
277
+ return text
278
+ }
58
279
  }
280
+ return text
59
281
  }
60
282
 
61
- type HttpRequestConfig = {
62
- ignoreWarnings?: string | RegExp | Array<string | RegExp>
283
+ function headersToRecord(headers: Headers): Record<string, string> {
284
+ const out: Record<string, string> = {}
285
+ headers.forEach((value, key) => {
286
+ out[key] = value
287
+ })
288
+ return out
63
289
  }
64
290
 
65
- /** @internal */
66
- export function defineHttpRequest(
67
- envMiddleware: Middlewares,
68
- config: HttpRequestConfig = {},
69
- ): Requester {
70
- return getIt([
71
- retry({shouldRetry}),
72
- ...envMiddleware,
73
- printWarnings(config),
74
- jsonRequest(),
75
- jsonResponse(),
76
- progress(),
77
- httpError,
78
- observable({implementation: Observable}),
79
- ])
291
+ function shouldRetryRequest(err: unknown, attempt: number, options: FetchRequestOptions): boolean {
292
+ // HTTP errors aren't usually retryable, but Content Lake gives us a few
293
+ // status codes where retrying *is* the right move.
294
+ if (err instanceof GetItHttpError) {
295
+ const isSafe = (options.method ?? 'GET') === 'GET' || options.method === 'HEAD'
296
+ const isQuery = (options.url ?? '').includes('/data/query')
297
+ const status = err.status
298
+ const retryableStatus = status === 429 || status === 502 || status === 503
299
+ if ((isSafe || isQuery) && retryableStatus) return true
300
+ return false
301
+ }
302
+
303
+ return isRetryableRequest(err, attempt, options)
80
304
  }
81
305
 
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- function shouldRetry(err: any, attempt: number, options: any) {
84
- // Allow opting out of retries
85
- if (options.maxRetries === 0) return false
86
-
87
- // By default `retry.shouldRetry` doesn't retry on server errors so we add our own logic.
88
-
89
- const isSafe = options.method === 'GET' || options.method === 'HEAD'
90
- const uri = options.uri || options.url
91
- const isQuery = uri.startsWith('/data/query')
92
- const isRetriableResponse =
93
- err.response &&
94
- (err.response.statusCode === 429 ||
95
- err.response.statusCode === 502 ||
96
- err.response.statusCode === 503)
97
-
98
- // We retry the following errors:
99
- // - 429 means that the request was rate limited. It's a bit difficult
100
- // to know exactly how long it makes sense to wait and/or how many
101
- // attempts we should retry, but the backoff should alleviate the
102
- // additional load.
103
- // - 502/503 can occur when certain components struggle to talk to their
104
- // upstream dependencies. This is most likely a temporary problem
105
- // and retrying makes sense.
106
-
107
- if ((isSafe || isQuery) && isRetriableResponse) return true
108
-
109
- return retry.shouldRetry(err, attempt, options)
306
+ function printWarnings(config: HttpRequestConfig): TransformMiddleware {
307
+ const seen: Record<string, boolean> = {}
308
+
309
+ const shouldIgnore = (message: string): boolean => {
310
+ if (config.ignoreWarnings === undefined) return false
311
+ const patterns = Array.isArray(config.ignoreWarnings)
312
+ ? config.ignoreWarnings
313
+ : [config.ignoreWarnings]
314
+ return patterns.some((pattern) =>
315
+ typeof pattern === 'string' ? message.includes(pattern) : pattern.test(message),
316
+ )
317
+ }
318
+
319
+ return {
320
+ afterResponse(response) {
321
+ const header = response.headers.get('x-sanity-warning')
322
+ if (!header) return response
323
+ // Multiple warnings can be comma-separated per HTTP header semantics.
324
+ for (const msg of header.split(',').map((m) => m.trim())) {
325
+ if (!msg || seen[msg] || shouldIgnore(msg)) continue
326
+ seen[msg] = true
327
+ // oxlint-disable-next-line no-console
328
+ console.warn(msg)
329
+ }
330
+ return response
331
+ },
332
+ }
110
333
  }
@@ -1,11 +1,34 @@
1
- import type {RequestOptions} from 'get-it'
1
+ import type {RequestOptions as GetItRequestOptions} from 'get-it'
2
2
 
3
3
  import type {Any} from '../types'
4
4
 
5
5
  const projectHeader = 'X-Sanity-Project-ID'
6
6
 
7
- export function requestOptions(config: Any, overrides: Any = {}): Omit<RequestOptions, 'url'> {
8
- const headers: Any = {}
7
+ /**
8
+ * The request shape the transport (`defineRequester`) consumes: get-it v9's
9
+ * own options, with `headers` pinned to the plain-record form this builder
10
+ * always produces.
11
+ *
12
+ * @internal
13
+ */
14
+ export type FetchRequest = Omit<GetItRequestOptions, 'headers'> & {
15
+ headers: Record<string, string>
16
+ }
17
+
18
+ /**
19
+ * Project the public request options (`timeout: 0` to disable,
20
+ * `withCredentials`, `maxRedirects`, the function/object-form `fetch`, ...)
21
+ * plus the client config into a get-it v9 fetch-shaped request. This is the
22
+ * single translation boundary between the client's public option names and
23
+ * the transport — everything below it speaks get-it v9.
24
+ *
25
+ * Reads the live client config, so reconfiguration via `client.config()` /
26
+ * `withConfig()` (token, headers, proxy, ...) applies to subsequent requests.
27
+ *
28
+ * @internal
29
+ */
30
+ export function requestOptions(config: Any, overrides: Any = {}): FetchRequest {
31
+ const headers: Record<string, string> = {}
9
32
 
10
33
  if (config.headers) {
11
34
  Object.assign(headers, config.headers)
@@ -13,29 +36,122 @@ export function requestOptions(config: Any, overrides: Any = {}): Omit<RequestOp
13
36
 
14
37
  const token = overrides.token || config.token
15
38
  if (token) {
16
- headers.Authorization = `Bearer ${token}`
39
+ headers['Authorization'] = `Bearer ${token}`
17
40
  }
18
41
 
19
42
  if (!overrides.useGlobalApi && !config.useProjectHostname && config.projectId) {
20
43
  headers[projectHeader] = config.projectId
21
44
  }
22
45
 
46
+ const request: FetchRequest = {
47
+ url: overrides.url,
48
+ headers: Object.assign(headers, overrides.headers || {}),
49
+ }
50
+
51
+ if (overrides.method) request.method = overrides.method
52
+ if (overrides.body !== undefined) request.body = overrides.body
53
+ if (overrides.query) request.query = expandQueryArrays(overrides.query)
54
+ if (overrides.signal) request.signal = overrides.signal
55
+
23
56
  const withCredentials = Boolean(
24
57
  typeof overrides.withCredentials === 'undefined'
25
58
  ? config.withCredentials
26
59
  : overrides.withCredentials,
27
60
  )
61
+ if (withCredentials) request.credentials = 'include'
28
62
 
63
+ // The public option is the legacy `maxRedirects` count, but only "follow"
64
+ // vs "don't" ever worked — fetch has no redirect budget.
65
+ if (typeof overrides.maxRedirects === 'number') {
66
+ request.redirect = overrides.maxRedirects === 0 ? 'manual' : 'follow'
67
+ }
68
+
69
+ // Public semantics: `0` disables the timeout (get-it uses `false`), default
70
+ // is five minutes.
29
71
  const timeout = typeof overrides.timeout === 'undefined' ? config.timeout : overrides.timeout
30
- return Object.assign({}, overrides, {
31
- headers: Object.assign({}, headers, overrides.headers || {}),
32
- timeout: typeof timeout === 'undefined' ? 5 * 60 * 1000 : timeout,
33
- proxy: overrides.proxy || config.proxy,
34
- json: true,
35
- withCredentials,
36
- fetch:
37
- typeof overrides.fetch === 'object' && typeof config.fetch === 'object'
38
- ? {...config.fetch, ...overrides.fetch}
39
- : overrides.fetch || config.fetch,
40
- })
72
+ request.timeout = typeof timeout === 'undefined' ? 5 * 60 * 1000 : timeout === 0 ? false : timeout
73
+
74
+ // `useAbortSignal: false` is set by the query path when the caller provided
75
+ // no signal of their own, and means no AbortSignal may reach the fetch
76
+ // init: Next.js' patched fetch opts a request out of React Request
77
+ // Memoization whenever `init.signal` is present, and get-it v9 implements
78
+ // timeouts via `AbortSignal.timeout()`. Switch to get-it's rejection-only
79
+ // timeout mode (`{signal: false}`): the deadline still rejects, but no
80
+ // signal reaches the fetch init and the underlying request is left running,
81
+ // so a memoizing fetch can still settle the shared promise for other
82
+ // consumers.
83
+ if (overrides.useAbortSignal === false && !request.signal) {
84
+ request.timeout =
85
+ typeof request.timeout === 'number' && request.timeout > 0
86
+ ? {total: request.timeout, signal: false}
87
+ : false
88
+ }
89
+
90
+ // The public `fetch` option is either a custom fetch implementation
91
+ // (function) or a bag of extra `RequestInit` fields (object) — Next.js App
92
+ // Router's `cache`/`next` caching options arrive as the latter. get-it's
93
+ // `fetch` option only accepts a function, so the init extras travel in
94
+ // `meta` and are merged into the effective fetch by the `applyFetchInit`
95
+ // middleware. (A boolean `fetch` was v8's "force the fetch transport"
96
+ // switch — a no-op now that fetch is the only transport.)
97
+ const fetchOption =
98
+ typeof overrides.fetch === 'object' && typeof config.fetch === 'object'
99
+ ? {...config.fetch, ...overrides.fetch}
100
+ : overrides.fetch || config.fetch
101
+ if (typeof fetchOption === 'function') {
102
+ request.fetch = fetchOption
103
+ } else if (typeof fetchOption === 'object' && fetchOption !== null) {
104
+ request.meta = {...request.meta, fetchInit: fetchOption}
105
+ }
106
+
107
+ // The config's fetch resolver supplies the transport for every request —
108
+ // the environment's own fetch by default, a proxy-configured one when an
109
+ // explicit `proxy` is set, or a caller-supplied resolver (the test suite
110
+ // injects its mock this way). A per-request `fetch` function wins over it.
111
+ // There is deliberately no per-request proxy.
112
+ if (!request.fetch && config.resolveFetch) {
113
+ request.fetch = config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
114
+ }
115
+
116
+ // Per-request retry cap/opt-out (`maxRetries: 0` disables retries) —
117
+ // honored natively by get-it's retry middleware, overriding the
118
+ // client-level maximum in both directions.
119
+ if (typeof overrides.maxRetries === 'number') {
120
+ request.maxRetries = overrides.maxRetries
121
+ }
122
+
123
+ // Lineage travels in `meta`; the Node middleware projects it onto the
124
+ // `x-sanity-lineage` header (merged with the `X_SANITY_LINEAGE` env var).
125
+ if (typeof config.lineage === 'string' && config.lineage) {
126
+ request.meta = {...request.meta, lineage: config.lineage}
127
+ }
128
+
129
+ return request
130
+ }
131
+
132
+ /**
133
+ * Expand array-valued query params into repeated keys.
134
+ *
135
+ * get-it v9 stringifies a plain object's values directly, so passing
136
+ * `{meta: ['palette', 'location']}` would produce `?meta=palette,location`
137
+ * — which Content Lake doesn't recognise. Repeated keys
138
+ * (`?meta=palette&meta=location`) are produced via `URLSearchParams`.
139
+ */
140
+ function expandQueryArrays(query: Any): FetchRequest['query'] {
141
+ if (query instanceof URLSearchParams) return query
142
+ if (!query || typeof query !== 'object') return query
143
+ if (!Object.values(query).some(Array.isArray)) return query
144
+
145
+ const params = new URLSearchParams()
146
+ for (const [key, value] of Object.entries(query)) {
147
+ if (value === undefined || value === null) continue
148
+ if (Array.isArray(value)) {
149
+ for (const item of value) {
150
+ if (item !== undefined && item !== null) params.append(key, `${item}`)
151
+ }
152
+ } else {
153
+ params.append(key, `${value}`)
154
+ }
155
+ }
156
+ return params
41
157
  }
@@ -1,15 +1,20 @@
1
1
  import defineCreateClientExports, {type ClientConfig, SanityClient} from './defineCreateClient'
2
2
  import {defineDeprecatedCreateClient} from './defineDeprecatedCreateClient'
3
- import envMiddleware from './http/browserMiddleware'
3
+ import environment from './http/nodeMiddleware'
4
+ import type {Requester} from './types'
4
5
 
5
6
  export * from './defineCreateClient'
6
7
 
7
- const exp = defineCreateClientExports<SanityClient, ClientConfig>(envMiddleware, SanityClient)
8
+ const exp = defineCreateClientExports<SanityClient, ClientConfig>(environment, SanityClient)
8
9
 
9
10
  /** @public */
10
- export const requester = exp.requester
11
+ export const requester: Requester = exp.requester
11
12
 
12
- /** @public */
13
+ /**
14
+ * @remarks
15
+ * As of API version `v2025-02-19`, the default perspective used by the client has changed from `raw` to `published`. {@link https://www.sanity.io/changelog/676aaa9d-2da6-44fb-abe5-580f28047c10|Changelog}
16
+ * @public
17
+ */
13
18
  export const createClient = exp.createClient
14
19
 
15
20
  /**
package/src/index.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import defineCreateClientExports, {type ClientConfig, SanityClient} from './defineCreateClient'
2
2
  import {defineDeprecatedCreateClient} from './defineDeprecatedCreateClient'
3
- import envMiddleware from './http/nodeMiddleware'
3
+ import environment from './http/browserMiddleware'
4
+ import type {Requester} from './types'
4
5
 
5
6
  export * from './defineCreateClient'
6
7
 
7
- const exp = defineCreateClientExports<SanityClient, ClientConfig>(envMiddleware, SanityClient)
8
+ const exp = defineCreateClientExports<SanityClient, ClientConfig>(environment, SanityClient)
8
9
 
9
10
  /** @public */
10
- export const requester = exp.requester
11
+ export const requester: Requester = exp.requester
11
12
 
12
13
  /**
13
14
  * @remarks