UNPKG

@sanity/client

Version:

Client for retrieving, creating and patching data from Sanity.io

158 lines (137 loc) 6.4 kB
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 }