@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
158 lines (137 loc) • 6.4 kB
text/typescript
import type {RequestOptions as GetItRequestOptions} from 'get-it'
import type {Any} from '../types'
const projectHeader = 'X-Sanity-Project-ID'
/**
* The request shape the transport (`defineRequester`) consumes: get-it v9's
* own options, with `headers` pinned to the plain-record form this builder
* always produces.
*
* @internal
*/
export type FetchRequest = Omit<GetItRequestOptions, 'headers'> & {
headers: Record<string, string>
}
/**
* Project the public request options (`timeout: 0` to disable,
* `withCredentials`, `maxRedirects`, the function/object-form `fetch`, ...)
* plus the client config into a get-it v9 fetch-shaped request. This is the
* single translation boundary between the client's public option names and
* the transport — everything below it speaks get-it v9.
*
* Reads the live client config, so reconfiguration via `client.config()` /
* `withConfig()` (token, headers, proxy, ...) applies to subsequent requests.
*
* @internal
*/
export function requestOptions(config: Any, overrides: Any = {}): FetchRequest {
const headers: Record<string, string> = {}
if (config.headers) {
Object.assign(headers, config.headers)
}
const token = overrides.token || config.token
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
if (!overrides.useGlobalApi && !config.useProjectHostname && config.projectId) {
headers[projectHeader] = config.projectId
}
const request: FetchRequest = {
url: overrides.url,
headers: Object.assign(headers, overrides.headers || {}),
}
if (overrides.method) request.method = overrides.method
if (overrides.body !== undefined) request.body = overrides.body
if (overrides.query) request.query = expandQueryArrays(overrides.query)
if (overrides.signal) request.signal = overrides.signal
const withCredentials = Boolean(
typeof overrides.withCredentials === 'undefined'
? config.withCredentials
: overrides.withCredentials,
)
if (withCredentials) request.credentials = 'include'
// The public option is the legacy `maxRedirects` count, but only "follow"
// vs "don't" ever worked — fetch has no redirect budget.
if (typeof overrides.maxRedirects === 'number') {
request.redirect = overrides.maxRedirects === 0 ? 'manual' : 'follow'
}
// Public semantics: `0` disables the timeout (get-it uses `false`), default
// is five minutes.
const timeout = typeof overrides.timeout === 'undefined' ? config.timeout : overrides.timeout
request.timeout = typeof timeout === 'undefined' ? 5 * 60 * 1000 : timeout === 0 ? false : timeout
// `useAbortSignal: false` is set by the query path when the caller provided
// no signal of their own, and means no AbortSignal may reach the fetch
// init: Next.js' patched fetch opts a request out of React Request
// Memoization whenever `init.signal` is present, and get-it v9 implements
// timeouts via `AbortSignal.timeout()`. Switch to get-it's rejection-only
// timeout mode (`{signal: false}`): the deadline still rejects, but no
// signal reaches the fetch init and the underlying request is left running,
// so a memoizing fetch can still settle the shared promise for other
// consumers.
if (overrides.useAbortSignal === false && !request.signal) {
request.timeout =
typeof request.timeout === 'number' && request.timeout > 0
? {total: request.timeout, signal: false}
: false
}
// The public `fetch` option is either a custom fetch implementation
// (function) or a bag of extra `RequestInit` fields (object) — Next.js App
// Router's `cache`/`next` caching options arrive as the latter. get-it's
// `fetch` option only accepts a function, so the init extras travel in
// `meta` and are merged into the effective fetch by the `applyFetchInit`
// middleware. (A boolean `fetch` was v8's "force the fetch transport"
// switch — a no-op now that fetch is the only transport.)
const fetchOption =
typeof overrides.fetch === 'object' && typeof config.fetch === 'object'
? {...config.fetch, ...overrides.fetch}
: overrides.fetch || config.fetch
if (typeof fetchOption === 'function') {
request.fetch = fetchOption
} else if (typeof fetchOption === 'object' && fetchOption !== null) {
request.meta = {...request.meta, fetchInit: fetchOption}
}
// The config's fetch resolver supplies the transport for every request —
// the environment's own fetch by default, a proxy-configured one when an
// explicit `proxy` is set, or a caller-supplied resolver (the test suite
// injects its mock this way). A per-request `fetch` function wins over it.
// There is deliberately no per-request proxy.
if (!request.fetch && config.resolveFetch) {
request.fetch = config.resolveFetch(typeof config.proxy === 'string' ? config.proxy : undefined)
}
// Per-request retry cap/opt-out (`maxRetries: 0` disables retries) —
// honored natively by get-it's retry middleware, overriding the
// client-level maximum in both directions.
if (typeof overrides.maxRetries === 'number') {
request.maxRetries = overrides.maxRetries
}
// Lineage travels in `meta`; the Node middleware projects it onto the
// `x-sanity-lineage` header (merged with the `X_SANITY_LINEAGE` env var).
if (typeof config.lineage === 'string' && config.lineage) {
request.meta = {...request.meta, lineage: config.lineage}
}
return request
}
/**
* Expand array-valued query params into repeated keys.
*
* get-it v9 stringifies a plain object's values directly, so passing
* `{meta: ['palette', 'location']}` would produce `?meta=palette,location`
* — which Content Lake doesn't recognise. Repeated keys
* (`?meta=palette&meta=location`) are produced via `URLSearchParams`.
*/
function expandQueryArrays(query: Any): FetchRequest['query'] {
if (query instanceof URLSearchParams) return query
if (!query || typeof query !== 'object') return query
if (!Object.values(query).some(Array.isArray)) return query
const params = new URLSearchParams()
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null) continue
if (Array.isArray(value)) {
for (const item of value) {
if (item !== undefined && item !== null) params.append(key, `${item}`)
}
} else {
params.append(key, `${value}`)
}
}
return params
}