@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
1 lines • 400 kB
Source Map (JSON)
{"version":3,"file":"index.browser.cjs","sources":["../src/util/codeFrame.ts","../src/http/errors.ts","../src/http/request.ts","../src/generateHelpUrl.ts","../src/validators.ts","../src/types.ts","../src/util/once.ts","../src/warnings.ts","../src/config.ts","../src/data/eventsource.ts","../src/util/getSelection.ts","../src/data/patch.ts","../src/data/transaction.ts","../src/http/requestOptions.ts","../src/data/encodeQueryString.ts","../src/data/dataMethods.ts","../src/agent/actions/generate.ts","../src/agent/actions/patch.ts","../src/agent/actions/prompt.ts","../src/agent/actions/transform.ts","../src/agent/actions/translate.ts","../src/agent/actions/AgentActionsClient.ts","../src/assets/AssetsClient.ts","../src/util/defaults.ts","../src/util/pick.ts","../src/data/eventsourcePolyfill.ts","../src/data/reconnectOnConnectionFailure.ts","../src/data/listen.ts","../src/util/shareReplayLatest.ts","../src/data/live.ts","../src/datasets/DatasetsClient.ts","../src/mediaLibrary/MediaLibraryVideoClient.ts","../src/projects/ProjectsClient.ts","../src/util/createVersionId.ts","../src/releases/createRelease.ts","../src/releases/ReleasesClient.ts","../src/users/UsersClient.ts","../src/SanityClient.ts","../src/defineCreateClient.ts","../src/defineDeprecatedCreateClient.ts","../src/http/browserMiddleware.ts","../src/index.browser.ts"],"sourcesContent":["/**\n * Inlined, modified version of the `codeFrameColumns` function from `@babel/code-frame`.\n * MIT-licensed - https://github.com/babel/babel/blob/main/LICENSE\n * Copyright (c) 2014-present Sebastian McKenzie and other contributors.\n */\ntype Location = {\n column: number\n line: number\n}\n\ntype NodeLocation = {\n start: Location\n end?: Location\n}\n\ntype GroqLocation = {\n start: number\n end?: number\n}\n\n/**\n * RegExp to test for newlines.\n */\n\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/\n\n/**\n * Extract what lines should be marked and highlighted.\n */\n\ntype MarkerLines = Record<number, true | [number, number]>\n\n/**\n * Highlight a code frame with the given location and message.\n *\n * @param query - The query to be highlighted.\n * @param location - The location of the error in the code/query.\n * @param message - Message to be displayed inline (if possible) next to the highlighted\n * location in the code. If it can't be positioned inline, it will be placed above the\n * code frame.\n * @returns The highlighted code frame.\n */\nexport function codeFrame(query: string, location: GroqLocation, message?: string): string {\n const lines = query.split(NEWLINE)\n const loc = {\n start: columnToLine(location.start, lines),\n end: location.end ? columnToLine(location.end, lines) : undefined,\n }\n\n const {start, end, markerLines} = getMarkerLines(loc, lines)\n\n const numberMaxWidth = `${end}`.length\n\n return query\n .split(NEWLINE, end)\n .slice(start, end)\n .map((line, index) => {\n const number = start + 1 + index\n const paddedNumber = ` ${number}`.slice(-numberMaxWidth)\n const gutter = ` ${paddedNumber} |`\n const hasMarker = markerLines[number]\n const lastMarkerLine = !markerLines[number + 1]\n if (!hasMarker) {\n return ` ${gutter}${line.length > 0 ? ` ${line}` : ''}`\n }\n\n let markerLine = ''\n if (Array.isArray(hasMarker)) {\n const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\\t]/g, ' ')\n const numberOfMarkers = hasMarker[1] || 1\n\n markerLine = [\n '\\n ',\n gutter.replace(/\\d/g, ' '),\n ' ',\n markerSpacing,\n '^'.repeat(numberOfMarkers),\n ].join('')\n\n if (lastMarkerLine && message) {\n markerLine += ' ' + message\n }\n }\n return ['>', gutter, line.length > 0 ? ` ${line}` : '', markerLine].join('')\n })\n .join('\\n')\n}\n\nfunction getMarkerLines(\n loc: NodeLocation,\n source: Array<string>,\n): {\n start: number\n end: number\n markerLines: MarkerLines\n} {\n const startLoc: Location = {...loc.start}\n const endLoc: Location = {...startLoc, ...loc.end}\n const linesAbove = 2\n const linesBelow = 3\n const startLine = startLoc.line ?? -1\n const startColumn = startLoc.column ?? 0\n const endLine = endLoc.line\n const endColumn = endLoc.column\n\n let start = Math.max(startLine - (linesAbove + 1), 0)\n let end = Math.min(source.length, endLine + linesBelow)\n\n if (startLine === -1) {\n start = 0\n }\n\n if (endLine === -1) {\n end = source.length\n }\n\n const lineDiff = endLine - startLine\n const markerLines: MarkerLines = {}\n\n if (lineDiff) {\n for (let i = 0; i <= lineDiff; i++) {\n const lineNumber = i + startLine\n\n if (!startColumn) {\n markerLines[lineNumber] = true\n } else if (i === 0) {\n const sourceLength = source[lineNumber - 1].length\n\n markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1]\n } else if (i === lineDiff) {\n markerLines[lineNumber] = [0, endColumn]\n } else {\n const sourceLength = source[lineNumber - i].length\n\n markerLines[lineNumber] = [0, sourceLength]\n }\n }\n } else {\n if (startColumn === endColumn) {\n if (startColumn) {\n markerLines[startLine] = [startColumn, 0]\n } else {\n markerLines[startLine] = true\n }\n } else {\n markerLines[startLine] = [startColumn, endColumn - startColumn]\n }\n }\n\n return {start, end, markerLines}\n}\n\nfunction columnToLine(column: number, lines: string[]): Location {\n let offset = 0\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1 // assume '\\n' after each line\n\n if (offset + lineLength > column) {\n return {\n line: i + 1, // 1-based line\n column: column - offset, // 0-based column\n }\n }\n\n offset += lineLength\n }\n\n // Fallback: beyond last line\n return {\n line: lines.length,\n column: lines[lines.length - 1]?.length ?? 0,\n }\n}\n","import type {HttpContext} from 'get-it'\n\nimport type {ActionError, Any, ErrorProps, MutationError, QueryParseError} from '../types'\nimport {codeFrame} from '../util/codeFrame'\nimport {isRecord} from '../util/isRecord'\n\nconst MAX_ITEMS_IN_ERROR_MESSAGE = 5\n\n/**\n * Shared properties for HTTP errors (eg both ClientError and ServerError)\n * Use `isHttpError` for type narrowing and accessing response properties.\n *\n * @public\n */\nexport interface HttpError {\n statusCode: number\n message: string\n response: {\n body: unknown\n url: string\n method: string\n headers: Record<string, string>\n statusCode: number\n statusMessage: string | null\n }\n}\n\n/**\n * Checks if the provided error is an HTTP error.\n *\n * @param error - The error to check.\n * @returns `true` if the error is an HTTP error, `false` otherwise.\n * @public\n */\nexport function isHttpError(error: unknown): error is HttpError {\n if (!isRecord(error)) {\n return false\n }\n\n const response = error.response\n if (\n typeof error.statusCode !== 'number' ||\n typeof error.message !== 'string' ||\n !isRecord(response)\n ) {\n return false\n }\n\n if (\n typeof response.body === 'undefined' ||\n typeof response.url !== 'string' ||\n typeof response.method !== 'string' ||\n typeof response.headers !== 'object' ||\n typeof response.statusCode !== 'number'\n ) {\n return false\n }\n\n return true\n}\n\n/** @public */\nexport class ClientError extends Error {\n response: ErrorProps['response']\n statusCode: ErrorProps['statusCode'] = 400\n responseBody: ErrorProps['responseBody']\n details: ErrorProps['details']\n\n constructor(res: Any, context?: HttpContext) {\n const props = extractErrorProps(res, context)\n super(props.message)\n Object.assign(this, props)\n }\n}\n\n/** @public */\nexport class ServerError extends Error {\n response: ErrorProps['response']\n statusCode: ErrorProps['statusCode'] = 500\n responseBody: ErrorProps['responseBody']\n details: ErrorProps['details']\n\n constructor(res: Any) {\n const props = extractErrorProps(res)\n super(props.message)\n Object.assign(this, props)\n }\n}\n\nfunction extractErrorProps(res: Any, context?: HttpContext): ErrorProps {\n const body = res.body\n const props = {\n response: res,\n statusCode: res.statusCode,\n responseBody: stringifyBody(body, res),\n message: '',\n details: undefined as Any,\n }\n\n // Fall back early if we didn't get a JSON object returned as expected\n if (!isRecord(body)) {\n props.message = httpErrorMessage(res, body)\n return props\n }\n\n const error = body.error\n\n // API/Boom style errors ({statusCode, error, message})\n if (typeof error === 'string' && typeof body.message === 'string') {\n props.message = `${error} - ${body.message}`\n return props\n }\n\n // Content Lake errors with a `error` prop being an object\n if (typeof error !== 'object' || error === null) {\n if (typeof error === 'string') {\n props.message = error\n } else if (typeof body.message === 'string') {\n props.message = body.message\n } else {\n props.message = httpErrorMessage(res, body)\n }\n return props\n }\n\n // Mutation errors (specifically)\n if (isMutationError(error) || isActionError(error)) {\n const allItems = error.items || []\n const items = allItems\n .slice(0, MAX_ITEMS_IN_ERROR_MESSAGE)\n .map((item) => item.error?.description)\n .filter(Boolean)\n let itemsStr = items.length ? `:\\n- ${items.join('\\n- ')}` : ''\n if (allItems.length > MAX_ITEMS_IN_ERROR_MESSAGE) {\n itemsStr += `\\n...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`\n }\n props.message = `${error.description}${itemsStr}`\n props.details = body.error\n return props\n }\n\n // Query parse errors\n if (isQueryParseError(error)) {\n const tag = context?.options?.query?.tag\n props.message = formatQueryParseError(error, tag)\n props.details = body.error\n return props\n }\n\n if ('description' in error && typeof error.description === 'string') {\n // Query/database errors ({error: {description, other, arb, props}})\n props.message = error.description\n props.details = error\n return props\n }\n\n // Other, more arbitrary errors\n props.message = httpErrorMessage(res, body)\n return props\n}\n\nfunction isMutationError(error: object): error is MutationError {\n return (\n 'type' in error &&\n error.type === 'mutationError' &&\n 'description' in error &&\n typeof error.description === 'string'\n )\n}\n\nfunction isActionError(error: object): error is ActionError {\n return (\n 'type' in error &&\n error.type === 'actionError' &&\n 'description' in error &&\n typeof error.description === 'string'\n )\n}\n\n/** @internal */\nexport function isQueryParseError(error: object): error is QueryParseError {\n return (\n isRecord(error) &&\n error.type === 'queryParseError' &&\n typeof error.query === 'string' &&\n typeof error.start === 'number' &&\n typeof error.end === 'number'\n )\n}\n\n/**\n * Formats a GROQ query parse error into a human-readable string.\n *\n * @param error - The error object containing details about the parse error.\n * @param tag - An optional tag to include in the error message.\n * @returns A formatted error message string.\n * @public\n */\nexport function formatQueryParseError(error: QueryParseError, tag?: string | null) {\n const {query, start, end, description} = error\n\n if (!query || typeof start === 'undefined') {\n return `GROQ query parse error: ${description}`\n }\n\n const withTag = tag ? `\\n\\nTag: ${tag}` : ''\n const framed = codeFrame(query, {start, end}, description)\n\n return `GROQ query parse error:\\n${framed}${withTag}`\n}\n\nfunction httpErrorMessage(res: Any, body: unknown) {\n const details = typeof body === 'string' ? ` (${sliceWithEllipsis(body, 100)})` : ''\n const statusMessage = res.statusMessage ? ` ${res.statusMessage}` : ''\n return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`\n}\n\nfunction stringifyBody(body: Any, res: Any) {\n const contentType = (res.headers['content-type'] || '').toLowerCase()\n const isJson = contentType.indexOf('application/json') !== -1\n return isJson ? JSON.stringify(body, null, 2) : body\n}\n\nfunction sliceWithEllipsis(str: string, max: number) {\n return str.length > max ? `${str.slice(0, max)}…` : str\n}\n\n/** @public */\nexport class CorsOriginError extends Error {\n projectId: string\n addOriginUrl?: URL\n\n constructor({projectId}: {projectId: string}) {\n super('CorsOriginError')\n this.name = 'CorsOriginError'\n this.projectId = projectId\n\n const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)\n if (typeof location !== 'undefined') {\n const {origin} = location\n url.searchParams.set('cors', 'add')\n url.searchParams.set('origin', origin)\n this.addOriginUrl = url\n this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`\n } else {\n this.message = `The current origin is not allowed to connect to the Live Content API. Change your configuration here: ${url}`\n }\n }\n}\n","import {getIt, type HttpContext, type Middlewares, type Requester} from 'get-it'\nimport {jsonRequest, jsonResponse, observable, progress, retry} from 'get-it/middleware'\nimport {Observable} from 'rxjs'\n\nimport type {Any} from '../types'\nimport {ClientError, ServerError} from './errors'\n\nconst httpError = {\n onResponse: (res: Any, context: HttpContext) => {\n if (res.statusCode >= 500) {\n throw new ServerError(res)\n } else if (res.statusCode >= 400) {\n throw new ClientError(res, context)\n }\n\n return res\n },\n}\n\nfunction printWarnings(config: {ignoreWarnings?: string | RegExp | Array<string | RegExp>} = {}) {\n const seen: Record<string, boolean> = {}\n\n // Helper function to check if a warning should be ignored\n const shouldIgnoreWarning = (message: string): boolean => {\n if (config.ignoreWarnings === undefined) return false\n\n const patterns = Array.isArray(config.ignoreWarnings)\n ? config.ignoreWarnings\n : [config.ignoreWarnings]\n\n return patterns.some((pattern) => {\n if (typeof pattern === 'string') {\n return message.includes(pattern)\n } else if (pattern instanceof RegExp) {\n return pattern.test(message)\n }\n return false\n })\n }\n\n return {\n onResponse: (res: Any) => {\n const warn = res.headers['x-sanity-warning']\n const warnings = Array.isArray(warn) ? warn : [warn]\n for (const msg of warnings) {\n if (!msg || seen[msg]) continue\n\n // Skip warnings that match ignore patterns\n if (shouldIgnoreWarning(msg)) {\n continue\n }\n\n seen[msg] = true\n console.warn(msg) // eslint-disable-line no-console\n }\n return res\n },\n }\n}\n\ntype HttpRequestConfig = {\n ignoreWarnings?: string | RegExp | Array<string | RegExp>\n}\n\n/** @internal */\nexport function defineHttpRequest(\n envMiddleware: Middlewares,\n config: HttpRequestConfig = {},\n): Requester {\n return getIt([\n retry({shouldRetry}),\n ...envMiddleware,\n printWarnings(config),\n jsonRequest(),\n jsonResponse(),\n progress(),\n httpError,\n observable({implementation: Observable}),\n ])\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction shouldRetry(err: any, attempt: number, options: any) {\n // Allow opting out of retries\n if (options.maxRetries === 0) return false\n\n // By default `retry.shouldRetry` doesn't retry on server errors so we add our own logic.\n\n const isSafe = options.method === 'GET' || options.method === 'HEAD'\n const uri = options.uri || options.url\n const isQuery = uri.startsWith('/data/query')\n const isRetriableResponse =\n err.response &&\n (err.response.statusCode === 429 ||\n err.response.statusCode === 502 ||\n err.response.statusCode === 503)\n\n // We retry the following errors:\n // - 429 means that the request was rate limited. It's a bit difficult\n // to know exactly how long it makes sense to wait and/or how many\n // attempts we should retry, but the backoff should alleviate the\n // additional load.\n // - 502/503 can occur when certain components struggle to talk to their\n // upstream dependencies. This is most likely a temporary problem\n // and retrying makes sense.\n\n if ((isSafe || isQuery) && isRetriableResponse) return true\n\n return retry.shouldRetry(err, attempt, options)\n}\n","const BASE_URL = 'https://www.sanity.io/help/'\n\nexport function generateHelpUrl(slug: string) {\n return BASE_URL + slug\n}\n","import type {Any, InitializedClientConfig, SanityDocumentStub} from './types'\n\nconst VALID_ASSET_TYPES = ['image', 'file']\nconst VALID_INSERT_LOCATIONS = ['before', 'after', 'replace']\n\nexport const dataset = (name: string) => {\n if (!/^(~[a-z0-9]{1}[-\\w]{0,63}|[a-z0-9]{1}[-\\w]{0,63})$/.test(name)) {\n throw new Error(\n 'Datasets can only contain lowercase characters, numbers, underscores and dashes, and start with tilde, and be maximum 64 characters',\n )\n }\n}\n\nexport const projectId = (id: string) => {\n if (!/^[-a-z0-9]+$/i.test(id)) {\n throw new Error('`projectId` can only contain only a-z, 0-9 and dashes')\n }\n}\n\nexport const validateAssetType = (type: string) => {\n if (VALID_ASSET_TYPES.indexOf(type) === -1) {\n throw new Error(`Invalid asset type: ${type}. Must be one of ${VALID_ASSET_TYPES.join(', ')}`)\n }\n}\n\nexport const validateObject = (op: string, val: Any) => {\n if (val === null || typeof val !== 'object' || Array.isArray(val)) {\n throw new Error(`${op}() takes an object of properties`)\n }\n}\n\nexport const validateDocumentId = (op: string, id: string) => {\n if (typeof id !== 'string' || !/^[a-z0-9_][a-z0-9_.-]{0,127}$/i.test(id) || id.includes('..')) {\n throw new Error(`${op}(): \"${id}\" is not a valid document ID`)\n }\n}\n\nexport const requireDocumentId = (op: string, doc: Record<string, Any>) => {\n if (!doc._id) {\n throw new Error(`${op}() requires that the document contains an ID (\"_id\" property)`)\n }\n\n validateDocumentId(op, doc._id)\n}\n\nexport const validateDocumentType = (op: string, type: string) => {\n if (typeof type !== 'string') {\n throw new Error(`\\`${op}()\\`: \\`${type}\\` is not a valid document type`)\n }\n}\n\nexport const requireDocumentType = (op: string, doc: Record<string, Any>) => {\n if (!doc._type) {\n throw new Error(`\\`${op}()\\` requires that the document contains a type (\\`_type\\` property)`)\n }\n\n validateDocumentType(op, doc._type)\n}\n\nexport const validateVersionIdMatch = (builtVersionId: string, document: SanityDocumentStub) => {\n if (document._id && document._id !== builtVersionId) {\n throw new Error(\n `The provided document ID (\\`${document._id}\\`) does not match the generated version ID (\\`${builtVersionId}\\`)`,\n )\n }\n}\n\nexport const validateInsert = (at: string, selector: string, items: Any[]) => {\n const signature = 'insert(at, selector, items)'\n if (VALID_INSERT_LOCATIONS.indexOf(at) === -1) {\n const valid = VALID_INSERT_LOCATIONS.map((loc) => `\"${loc}\"`).join(', ')\n throw new Error(`${signature} takes an \"at\"-argument which is one of: ${valid}`)\n }\n\n if (typeof selector !== 'string') {\n throw new Error(`${signature} takes a \"selector\"-argument which must be a string`)\n }\n\n if (!Array.isArray(items)) {\n throw new Error(`${signature} takes an \"items\"-argument which must be an array`)\n }\n}\n\nexport const hasDataset = (config: InitializedClientConfig): string => {\n // Check if dataset is directly on the config\n if (config.dataset) {\n return config.dataset\n }\n\n // Check if dataset is in resource configuration\n // Note: ~experimental_resource is normalized to resource during client initialization\n const resource = config.resource\n if (resource && resource.type === 'dataset') {\n const segments = resource.id.split('.')\n if (segments.length !== 2) {\n throw new Error('Dataset resource ID must be in the format \"project.dataset\"')\n }\n return segments[1]\n }\n\n throw new Error('`dataset` must be provided to perform queries')\n}\n\nexport const requestTag = (tag: string) => {\n if (typeof tag !== 'string' || !/^[a-z0-9._-]{1,75}$/i.test(tag)) {\n throw new Error(\n `Tag can only contain alphanumeric characters, underscores, dashes and dots, and be between one and 75 characters long.`,\n )\n }\n\n return tag\n}\n\nexport const resourceConfig = (config: InitializedClientConfig): void => {\n // Note: ~experimental_resource is normalized to resource during client initialization\n const resource = config.resource\n if (!resource) {\n throw new Error('`resource` must be provided to perform resource queries')\n }\n const {type, id} = resource\n\n switch (type) {\n case 'dataset': {\n const segments = id.split('.')\n if (segments.length !== 2) {\n throw new Error('Dataset resource ID must be in the format \"project.dataset\"')\n }\n return\n }\n case 'dashboard':\n case 'media-library':\n case 'canvas': {\n return\n }\n default:\n // @ts-expect-error - handle all supported resource types\n throw new Error(`Unsupported resource type: ${type.toString()}`)\n }\n}\n\nexport const resourceGuard = (service: string, config: InitializedClientConfig): void => {\n // Note: ~experimental_resource is normalized to resource during client initialization\n const resource = config.resource\n if (resource) {\n throw new Error(`\\`${service}\\` does not support resource-based operations`)\n }\n}\n","// deno-lint-ignore-file no-empty-interface\n/* eslint-disable @typescript-eslint/no-empty-object-type */\n\nimport type {Requester} from 'get-it'\n\nimport type {InitializedStegaConfig, StegaConfig} from './stega/types'\n\n/**\n * Used to tag types that is set to `any` as a temporary measure, but should be replaced with proper typings in the future\n * @internal\n */\nexport type Any = any // eslint-disable-line @typescript-eslint/no-explicit-any\n\ndeclare global {\n // Declare empty stub interfaces for environments where \"dom\" lib is not included\n interface File {}\n}\n\n/** @public */\nexport type UploadBody = File | Blob | Buffer | NodeJS.ReadableStream\n\n/** @public */\nexport interface RequestOptions {\n timeout?: number\n token?: string\n tag?: string\n headers?: Record<string, string>\n method?: string\n query?: Any\n body?: Any\n signal?: AbortSignal\n}\n\n/**\n * @public\n * @deprecated – The `r`-prefix is not required, use `string` instead\n */\nexport type ReleaseId = `r${string}`\n\n/**\n * @deprecated use 'drafts' instead\n */\ntype DeprecatedPreviewDrafts = 'previewDrafts'\n\n/** @public */\nexport type StackablePerspective = ('published' | 'drafts' | string) & {}\n\n/** @public */\nexport type ClientPerspective =\n | DeprecatedPreviewDrafts\n | 'published'\n | 'drafts'\n | 'raw'\n | StackablePerspective[]\n\ntype ClientConfigResource =\n | {\n type: 'canvas'\n id: string\n }\n | {\n type: 'media-library'\n id: string\n }\n | {\n type: 'dataset'\n id: string\n }\n | {\n type: 'dashboard'\n id: string\n }\n\n/** @public */\nexport interface ClientConfig {\n projectId?: string\n dataset?: string\n /** @defaultValue true */\n useCdn?: boolean\n token?: string\n\n /**\n * Configure the client to work with a specific Sanity resource (Media Library, Canvas, etc.)\n * @remarks\n * This allows the client to interact with resources beyond traditional project datasets.\n * When configured, methods like `fetch()`, `assets.upload()`, and mutations will operate on the specified resource.\n * @example\n * ```ts\n * createClient({\n * resource: {\n * type: 'media-library',\n * id: 'your-media-library-id'\n * }\n * })\n * ```\n */\n resource?: ClientConfigResource\n\n /**\n * @deprecated Use `resource` instead\n * @internal\n */\n '~experimental_resource'?: ClientConfigResource\n\n /**\n * What perspective to use for the client. See {@link https://www.sanity.io/docs/perspectives|perspective documentation}\n * @remarks\n * As of API version `v2025-02-19`, the default perspective has changed from `raw` to `published`. {@link https://www.sanity.io/changelog/676aaa9d-2da6-44fb-abe5-580f28047c10|Changelog}\n * @defaultValue 'published'\n */\n perspective?: ClientPerspective\n apiHost?: string\n\n /**\n @remarks\n * As of API version `v2025-02-19`, the default perspective has changed from `raw` to `published`. {@link https://www.sanity.io/changelog/676aaa9d-2da6-44fb-abe5-580f28047c10|Changelog}\n */\n apiVersion?: string\n proxy?: string\n\n /**\n * Optional request tag prefix for all request tags\n */\n requestTagPrefix?: string\n\n /**\n * Optional default headers to include with all requests\n *\n * @remarks request-specific headers will override any default headers with the same name.\n */\n headers?: Record<string, string>\n\n ignoreBrowserTokenWarning?: boolean\n /**\n * Ignore specific warning messages from the client.\n *\n * @remarks\n * - String values perform substring matching (not exact matching) against warning messages\n * - RegExp values are tested against the full warning message\n * - Array values allow multiple patterns to be specified\n *\n * @example\n * ```typescript\n * // Ignore warnings containing \"experimental\"\n * ignoreWarnings: 'experimental'\n *\n * // Ignore multiple warning types\n * ignoreWarnings: ['experimental', 'deprecated']\n *\n * // Use regex for exact matching\n * ignoreWarnings: /^This is an experimental API version$/\n *\n * // Mix strings and regex patterns\n * ignoreWarnings: ['rate limit', /^deprecated/i]\n * ```\n */\n ignoreWarnings?: string | RegExp | Array<string | RegExp>\n withCredentials?: boolean\n allowReconfigure?: boolean\n timeout?: number\n\n /** Number of retries for requests. Defaults to 5. */\n maxRetries?: number\n\n /**\n * The amount of time, in milliseconds, to wait before retrying, given an attemptNumber (starting at 0).\n *\n * Defaults to exponential back-off, starting at 100ms, doubling for each attempt, together with random\n * jitter between 0 and 100 milliseconds. More specifically the following algorithm is used:\n *\n * Delay = 100 * 2^attemptNumber + randomNumberBetween0and100\n */\n retryDelay?: (attemptNumber: number) => number\n\n /**\n * @deprecated Don't use\n */\n useProjectHostname?: boolean\n\n /**\n * @deprecated Don't use\n */\n requester?: Requester\n\n /**\n * Adds a `resultSourceMap` key to the API response, with the type `ContentSourceMap`\n */\n resultSourceMap?: boolean | 'withKeyArraySelector'\n /**\n *@deprecated set `cache` and `next` options on `client.fetch` instead\n */\n fetch?:\n | {\n cache?: ResponseQueryOptions['cache']\n next?: ResponseQueryOptions['next']\n }\n | boolean\n /**\n * Options for how, if enabled, Content Source Maps are encoded into query results using steganography\n */\n stega?: StegaConfig | boolean\n /**\n * Lineage token for recursion control\n */\n lineage?: string\n}\n\n/** @public */\nexport interface InitializedClientConfig extends ClientConfig {\n // These are required in the initialized config\n apiHost: string\n apiVersion: string\n useProjectHostname: boolean\n useCdn: boolean\n // These are added by the initConfig function\n /**\n * @deprecated Internal, don't use\n */\n isDefaultApi: boolean\n /**\n * @deprecated Internal, don't use\n */\n url: string\n /**\n * @deprecated Internal, don't use\n */\n cdnUrl: string\n /**\n * The fully initialized stega config, can be used to check if stega is enabled\n */\n stega: InitializedStegaConfig\n /**\n * Default headers to include with all requests\n *\n * @remarks request-specific headers will override any default headers with the same name.\n */\n headers?: Record<string, string>\n}\n\n/** @public */\nexport type AssetMetadataType =\n | 'location'\n | 'exif'\n | 'image'\n | 'palette'\n | 'lqip'\n | 'blurhash'\n | 'thumbhash'\n | 'none'\n\n/** @public */\nexport interface UploadClientConfig {\n /**\n * Optional request tag for the upload\n */\n tag?: string\n\n /**\n * Whether or not to preserve the original filename (default: true)\n */\n preserveFilename?: boolean\n\n /**\n * Filename for this file (optional)\n */\n filename?: string\n\n /**\n * Milliseconds to wait before timing the request out\n */\n timeout?: number\n\n /**\n * Mime type of the file\n */\n contentType?: string\n\n /**\n * Array of metadata parts to extract from asset\n */\n extract?: AssetMetadataType[]\n\n /**\n * Optional freeform label for the asset. Generally not used.\n */\n label?: string\n\n /**\n * Optional title for the asset\n */\n title?: string\n\n /**\n * Optional description for the asset\n */\n description?: string\n\n /**\n * The credit to person(s) and/or organization(s) required by the supplier of the asset to be used when published\n */\n creditLine?: string\n\n /**\n * Source data (when the asset is from an external service)\n */\n source?: {\n /**\n * The (u)id of the asset within the source, i.e. 'i-f323r1E'\n */\n id: string\n\n /**\n * The name of the source, i.e. 'unsplash'\n */\n name: string\n\n /**\n * A url to where to find the asset, or get more info about it in the source\n */\n url?: string\n }\n}\n\n/** @internal */\nexport interface SanityReference {\n _ref: string\n}\n\n/** @internal */\nexport type SanityDocument<T extends Record<string, Any> = Record<string, Any>> = {\n [P in keyof T]: T[P]\n} & {\n _id: string\n _rev: string\n _type: string\n _createdAt: string\n _updatedAt: string\n /**\n * Present when `perspective` is set to `previewDrafts`\n */\n _originalId?: string\n}\n\n/** @internal */\nexport interface SanityAssetDocument extends SanityDocument {\n url: string\n path: string\n size: number\n assetId: string\n mimeType: string\n sha1hash: string\n extension: string\n uploadId?: string\n originalFilename?: string\n}\n\n/** @internal */\nexport interface SanityImagePalette {\n background: string\n foreground: string\n population: number\n title: string\n}\n\n/** @internal */\nexport interface SanityImageAssetDocument extends SanityAssetDocument {\n metadata: {\n _type: 'sanity.imageMetadata'\n hasAlpha: boolean\n isOpaque: boolean\n lqip?: string\n blurHash?: string\n thumbHash?: string\n dimensions: {\n _type: 'sanity.imageDimensions'\n aspectRatio: number\n height: number\n width: number\n }\n palette?: {\n _type: 'sanity.imagePalette'\n darkMuted?: SanityImagePalette\n darkVibrant?: SanityImagePalette\n dominant?: SanityImagePalette\n lightMuted?: SanityImagePalette\n lightVibrant?: SanityImagePalette\n muted?: SanityImagePalette\n vibrant?: SanityImagePalette\n }\n image?: {\n _type: 'sanity.imageExifTags'\n [key: string]: Any\n }\n exif?: {\n _type: 'sanity.imageExifMetadata'\n [key: string]: Any\n }\n }\n}\n\n/** @public */\nexport interface ErrorProps {\n message: string\n response: Any\n statusCode: number\n responseBody: Any\n details: Any\n}\n\n/** @public */\nexport type HttpRequest = {\n (options: RequestOptions, requester: Requester): ReturnType<Requester>\n}\n\n/** @internal */\nexport interface RequestObservableOptions extends Omit<RequestOptions, 'url'> {\n url?: string\n uri?: string\n canUseCdn?: boolean\n useCdn?: boolean\n tag?: string\n returnQuery?: boolean\n resultSourceMap?: boolean | 'withKeyArraySelector'\n perspective?: ClientPerspective\n lastLiveEventId?: string\n cacheMode?: 'noStale'\n}\n\n/** @public */\nexport interface ProgressEvent {\n type: 'progress'\n stage: 'upload' | 'download'\n percent: number\n total?: number\n loaded?: number\n lengthComputable: boolean\n}\n\n/** @public */\nexport interface ResponseEvent<T = unknown> {\n type: 'response'\n body: T\n url: string\n method: string\n statusCode: number\n statusMessage?: string\n headers: Record<string, string>\n}\n\n/** @public */\nexport type HttpRequestEvent<T = unknown> = ResponseEvent<T> | ProgressEvent\n\n/** @internal */\nexport interface AuthProvider {\n name: string\n title: string\n url: string\n}\n\n/** @internal */\nexport type AuthProviderResponse = {providers: AuthProvider[]}\n\n/** @public */\nexport type DatasetAclMode = 'public' | 'private' | 'custom'\n\n/** @public */\nexport type DatasetResponse = {datasetName: string; aclMode: DatasetAclMode}\n/** @public */\nexport type DatasetsResponse = {\n name: string\n aclMode: DatasetAclMode\n createdAt: string\n createdByUserId: string\n addonFor: string | null\n datasetProfile: string\n features: string[]\n tags: string[]\n}[]\n\n/** @public */\nexport interface SanityProjectMember {\n id: string\n role: string\n isRobot: boolean\n isCurrentUser: boolean\n}\n\n/** @public */\nexport interface SanityProject {\n id: string\n displayName: string\n /**\n * @deprecated Use the `/user-applications` endpoint instead, which lists all deployed studios/applications\n * @see https://www.sanity.io/help/studio-host-user-applications\n */\n studioHost: string | null\n organizationId: string | null\n isBlocked: boolean\n isDisabled: boolean\n isDisabledByUser: boolean\n createdAt: string\n pendingInvites?: number\n maxRetentionDays?: number\n members: SanityProjectMember[]\n metadata: {\n cliInitializedAt?: string\n color?: string\n /**\n * @deprecated Use the `/user-applications` endpoint instead, which lists all deployed studios/applications\n * @see https://www.sanity.io/help/studio-host-user-applications\n */\n externalStudioHost?: string\n }\n}\n\n/** @public */\nexport interface SanityUser {\n id: string\n projectId: string\n displayName: string\n familyName: string | null\n givenName: string | null\n middleName: string | null\n imageUrl: string | null\n createdAt: string\n updatedAt: string\n isCurrentUser: boolean\n}\n\n/** @public */\nexport interface CurrentSanityUser {\n id: string\n name: string\n email: string\n profileImage: string | null\n role: string\n provider: string\n}\n\n/** @public */\nexport type SanityDocumentStub<T extends Record<string, Any> = Record<string, Any>> = {\n [P in keyof T]: T[P]\n} & {\n _type: string\n}\n\n/** @public */\nexport type IdentifiedSanityDocumentStub<T extends Record<string, Any> = Record<string, Any>> = {\n [P in keyof T]: T[P]\n} & {\n _id: string\n} & SanityDocumentStub\n\n/** @internal */\nexport type InsertPatch =\n | {before: string; items: Any[]}\n | {after: string; items: Any[]}\n | {replace: string; items: Any[]}\n\n// Note: this is actually incorrect/invalid, but implemented as-is for backwards compatibility\n/** @internal */\nexport interface PatchOperations {\n set?: {[key: string]: Any}\n setIfMissing?: {[key: string]: Any}\n diffMatchPatch?: {[key: string]: Any}\n unset?: string[]\n inc?: {[key: string]: number}\n dec?: {[key: string]: number}\n insert?: InsertPatch\n ifRevisionID?: string\n}\n\n/** @public */\nexport interface QueryParams {\n /* eslint-disable @typescript-eslint/no-explicit-any */\n [key: string]: any\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n body?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n cache?: 'next' extends keyof RequestInit ? never : any\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n filterResponse?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n headers?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n method?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n next?: 'next' extends keyof RequestInit ? never : any\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n perspective?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n query?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n resultSourceMap?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n returnQuery?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n signal?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n stega?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n tag?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n timeout?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n token?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n useCdn?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n lastLiveEventId?: never\n /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */\n cacheMode?: never\n /* eslint-enable @typescript-eslint/no-explicit-any */\n}\n\n/**\n * This type can be used with `client.fetch` to indicate that the query has no GROQ parameters.\n * @public\n */\nexport type QueryWithoutParams = Record<string, never> | undefined\n\n/** @internal */\nexport type MutationSelectionQueryParams = {[key: string]: Any}\n/** @internal */\nexport type MutationSelection =\n | {query: string; params?: MutationSelectionQueryParams}\n | {id: string | string[]}\n/** @internal */\nexport type PatchSelection = string | string[] | MutationSelection\n/** @internal */\nexport type PatchMutationOperation = PatchOperations & MutationSelection\n\n/** @public */\nexport type Mutation<R extends Record<string, Any> = Record<string, Any>> =\n | {create: SanityDocumentStub<R>}\n | {createOrReplace: IdentifiedSanityDocumentStub<R>}\n | {createIfNotExists: IdentifiedSanityDocumentStub<R>}\n | {delete: MutationSelection}\n | {patch: PatchMutationOperation}\n\n/** @public */\nexport type ReleaseAction =\n | CreateReleaseAction\n | EditReleaseAction\n | PublishReleaseAction\n | ArchiveReleaseAction\n | UnarchiveReleaseAction\n | ScheduleReleaseAction\n | UnscheduleReleaseAction\n | DeleteReleaseAction\n | ImportReleaseAction\n\n/** @public */\nexport type VersionAction =\n | CreateVersionAction\n | DiscardVersionAction\n | ReplaceVersionAction\n | UnpublishVersionAction\n\n/** @public */\nexport type Action =\n | CreateAction\n | ReplaceDraftAction\n | EditAction\n | DeleteAction\n | DiscardAction\n | PublishAction\n | UnpublishAction\n | VersionAction\n | ReleaseAction\n\n/** @public */\nexport type ImportReleaseAction =\n | {\n actionType: 'sanity.action.release.import'\n attributes: IdentifiedSanityDocumentStub\n releaseId: string\n ifExists: 'fail' | 'ignore' | 'replace'\n }\n | {\n actionType: 'sanity.action.release.import'\n document: IdentifiedSanityDocumentStub\n releaseId: string\n ifExists: 'fail' | 'ignore' | 'replace'\n }\n\n/**\n * Creates a new release under the given id, with metadata.\n *\n * @public\n */\nexport interface CreateReleaseAction {\n actionType: 'sanity.action.release.create'\n releaseId: string\n metadata?: Partial<ReleaseDocument['metadata']>\n}\n\n/**\n * Edits an existing release, updating the metadata.\n *\n * @public\n */\nexport interface EditReleaseAction {\n actionType: 'sanity.action.release.edit'\n releaseId: string\n patch: PatchOperations\n}\n\n/**\n * Publishes all documents in a release at once.\n *\n * @public\n */\nexport interface PublishReleaseAction {\n actionType: 'sanity.action.release.publish'\n releaseId: string\n}\n\n/**\n * Archives an `active` release, and deletes all the release documents.\n *\n * @public\n */\nexport interface ArchiveReleaseAction {\n actionType: 'sanity.action.release.archive'\n releaseId: string\n}\n\n/**\n * Unarchived an `archived` release, and restores all the release documents.\n *\n * @public\n */\nexport interface UnarchiveReleaseAction {\n actionType: 'sanity.action.release.unarchive'\n releaseId: string\n}\n\n/**\n * Queues release for publishing at the given future time.\n *\n * @public\n */\nexport interface ScheduleReleaseAction {\n actionType: 'sanity.action.release.schedule'\n releaseId: string\n publishAt: string\n}\n\n/**\n * Unschedules a `scheduled` release, stopping it from being published.\n *\n * @public\n */\nexport interface UnscheduleReleaseAction {\n actionType: 'sanity.action.release.unschedule'\n releaseId: string\n}\n\n/**\n * Deletes a `archived` or `published` release, and all the release documents versions.\n *\n * @public\n */\nexport interface DeleteReleaseAction {\n actionType: 'sanity.action.release.delete'\n releaseId: string\n}\n\n/**\n * Creates a new version of an existing document.\n *\n * If the `document` is provided, the version is created from the document\n * attached to the release as given by `document._id`\n *\n * If the `baseId` and `versionId` are provided, the version is created from the base document\n * and the version is attached to the release as given by `publishedId` and `versionId`\n *\n * @public\n */\nexport type CreateVersionAction = {\n actionType: 'sanity.action.document.version.create'\n publishedId: string\n} & (\n | {\n document: IdentifiedSanityDocumentStub\n }\n | {\n baseId: string\n versionId: string\n ifBaseRevisionId?: string\n }\n)\n\n/**\n * Delete a version of a document.\n *\n * @public\n */\nexport interface DiscardVersionAction {\n actionType: 'sanity.action.document.version.discard'\n versionId: string\n purge?: boolean\n}\n\n/**\n * Replace an existing version of a document.\n *\n * @public\n */\nexport interface ReplaceVersionAction {\n actionType: 'sanity.action.document.version.replace'\n document: IdentifiedSanityDocumentStub\n}\n\n/**\n * Identify that a version of a document should be unpublished when\n * the release that version is contained within is published.\n *\n * @public\n */\nexport interface UnpublishVersionAction {\n actionType: 'sanity.action.document.version.unpublish'\n versionId: string\n publishedId: string\n}\n\n/**\n * Creates a new draft document. The published version of the document must not already exist.\n * If the draft version of the document already exists the action will fail by default, but\n * this can be adjusted to instead leave the existing document in place.\n *\n * @public\n */\nexport type CreateAction = {\n actionType: 'sanity.action.document.create'\n\n /**\n * ID of the published document to create a draft for.\n */\n publishedId: string\n\n /**\n * Document to create. Requires a `_type` property.\n */\n attributes: IdentifiedSanityDocumentStub\n\n /**\n * ifExists controls what to do if the draft already exists\n */\n ifExists: 'fail' | 'ignore'\n}\n\n/**\n * Replaces an existing draft document.\n * At least one of the draft or published versions of the document must exist.\n *\n * @public\n * @deprecated Use {@link ReplaceVersionAction} instead\n */\nexport type ReplaceDraftAction = {\n actionType: 'sanity.action.document.replaceDraft'\n\n /**\n * Published document ID to create draft from, if draft does not exist\n */\n publishedId: string\n\n /**\n * Document to create if it does not already exist. Requires `_id` and `_type` properties.\n */\n attributes: IdentifiedSanityDocumentStub\n}\n\n/**\n * Modifies an existing draft document.\n * It applies the given patch to the document referenced by draftId.\n * If there is no such document then one is created using the current state of the published version and then that is updated accordingly.\n *\n * @public\n */\nexport type EditAction = {\n actionType: 'sanity.action.document.edit'\n\n /**\n * Draft document ID to edit\n */\n draftId: string\n\n /**\n * Published document ID to create draft from, if draft does not exist\n */\n publishedId: string\n\n /**\n * Patch operations to apply\n */\n patch: PatchOperations\n}\n\n/**\n * Deletes the published version of a document and optionally some (likely all known) draft versions.\n * If any draft version exists that is not specified for deletion this is an error.\n * If the purge flag is set then the document history is also deleted.\n *\n * @public\n */\nexport type DeleteAction = {\n actionType: 'sanity.action.document.delete'\n\n /**\n * Published document ID to delete\n */\n publishedId: string\n\n /**\n * Draft document ID to delete\n */\n includeDrafts: string[]\n\n /**\n * Delete document history\n */\n purge?: boolean\n}\n\n/**\n * Delete the draft version of a document.\n * It is an error if it does not exist. If the purge flag is set, the document history is also deleted.\n *\n * @public\n * @deprecated Use {@link DiscardVersionAction} instead\n */\nexport type DiscardAction = {\n actionType: 'sanity.action.document.discard'\n\n /**\n * Draft document ID to delete\n */\n draftId: string\n\n /**\n * Delete document history\n */\n purge?: boolean\n}\n\n/**\n * Publishes a draft document.\n * If a published version of the document already exists this is replaced by the current draft document.\n * In either case the draft document is deleted.\n * The optional revision id parameters can be used for optimistic locking to ensure\n * that the draft and/or published versions of the document have not been changed by another client.\n *\n * @public\n */\nexport type PublishAction = {\n actionType: 'sanity.action.document.publish'\n\n /**\n * Draft document ID to publish\n */\n draftId: string\n\n /**\n * Draft revision ID to match\n */\n ifDraftRevisionId?: string\n\n /**\n * Published document ID to replace\n */\n publishedId: string\n\n /**\n * Published revision ID to match\n */\n ifPublishedRevisionId?: string\n}\n\n/**\n * Retract a published document.\n * If there is no draft version then this is created from the published version.\n * In either case the published version is deleted.\n *\n * @public\n */\nexport type UnpublishAction = {\n actionType: 'sanity.action.document.unpublish'\n\n /**\n * Draft document ID to replace the published document with\n */\n draftId: string\n\n /**\n * Published document ID to delete\n */\n publishedId: string\n}\n\n/**\n * A mutation was performed. Note that when updating multiple documents in a transaction,\n * each document affected will get a separate mutation event.\n *\n * @public\n */\nexport type MutationEvent<R extends Record<string, Any> = Record<string, Any>> = {\n type: 'mutation'\n\n /**\n * The ID of the document that was affected\n */\n documentId: string\n\n /**\n * A unique ID for this event\n */\n eventId: string\n\n /**\n * The user ID of the user that performed the mutation\n */\n identity: string\n\n /**\n * An array of mutations that were performed. Note that this can differ slightly from the\n * mutations sent to the server, as the server may perform some mutations automatically.\n */\n mutations: Mutation[]\n\n /**\n * The revision ID of the document before the mutation was performed\n */\n previousRev?: string\n\n /**\n * The revision ID of the document after the mutation was performed\n */\n resultRev?: string\n\n /**\n * The document as it looked after the mutation was performed. This is only included if\n * the listener was configured with `includeResult: true`.\n */\n result?: SanityDocument<R>\n\n /**\n * The document as it looked before the mutation was performed. This is only included if\n * the listener was configured with `includePreviousRevision: true`.\n */\n previous?: SanityDocument<R> | null\n\n /**\n * The effects of the mutation, if the listener was configured with `effectFormat: 'mendoza'`.\n * Object with `apply` and `revert` arrays, see {@link https://github.com/sanity-io/mendoza}.\n */\n effects?: {apply: unknown[]; revert: unknown[]}\n\n /**\n * A timestamp for when the mutation was performed\n */\n timestamp: string\n\n /**\n * The transaction ID for the mutation\n */\n transactionId: string\n\n /**\n * The type of transition the document went through.\n *\n * - `update` means the document was previously part of the subscribed set of documents,\n * and still is.\n * - `appear` means the document was not previously part of the subscribed set of documents,\n * but is now. This can happen both on create or if updating to a state where it now matches\n * the filter provided to the listener.\n * - `disappear` means the document was previously part