@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
1 lines • 326 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/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/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/** @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() {\n const seen: Record<string, boolean> = {}\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 seen[msg] = true\n console.warn(msg) // eslint-disable-line no-console\n }\n return res\n },\n }\n}\n\n/** @internal */\nexport function defineHttpRequest(envMiddleware: Middlewares): Requester {\n return getIt([\n retry({shouldRetry}),\n ...envMiddleware,\n printWarnings(),\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 if (!config.dataset) {\n throw new Error('`dataset` must be provided to perform queries')\n }\n\n return config.dataset || ''\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 if (!config['~experimental_resource']) {\n throw new Error('`resource` must be provided to perform resource queries')\n }\n const {type, id} = config['~experimental_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 if (config['~experimental_resource']) {\n throw new Error(`\\`${service}\\` does not support resource-based operations`)\n }\n}\n","import type {Any} from '../types'\n\nexport function once(fn: Any) {\n let didCall = false\n let returnValue: Any\n return (...args: Any[]) => {\n if (didCall) {\n return returnValue\n }\n returnValue = fn(...args)\n didCall = true\n return returnValue\n }\n}\n","import {generateHelpUrl} from './generateHelpUrl'\nimport {type Any} from './types'\nimport {once} from './util/once'\n\nconst createWarningPrinter = (message: string[]) =>\n // eslint-disable-next-line no-console\n once((...args: Any[]) => console.warn(message.join(' '), ...args))\n\nexport const printCdnAndWithCredentialsWarning = createWarningPrinter([\n `Because you set \\`withCredentials\\` to true, we will override your \\`useCdn\\``,\n `setting to be false since (cookie-based) credentials are never set on the CDN`,\n])\n\nexport const printCdnWarning = createWarningPrinter([\n `Since you haven't set a value for \\`useCdn\\`, we will deliver content using our`,\n `global, edge-cached API-CDN. If you wish to have content delivered faster, set`,\n `\\`useCdn: false\\` to use the Live API. Note: You may incur higher costs using the live API.`,\n])\n\nexport const printCdnPreviewDraftsWarning = createWarningPrinter([\n `The Sanity client is configured with the \\`perspective\\` set to \\`drafts\\` or \\`previewDrafts\\`, which doesn't support the API-CDN.`,\n `The Live API will be used instead. Set \\`useCdn: false\\` in your configuration to hide this warning.`,\n])\n\nexport const printPreviewDraftsDeprecationWarning = createWarningPrinter([\n `The \\`previewDrafts\\` perspective has been renamed to \\`drafts\\` and will be removed in a future API version`,\n])\n\nexport const printBrowserTokenWarning = createWarningPrinter([\n 'You have configured Sanity client to use a token in the browser. This may cause unintentional security issues.',\n `See ${generateHelpUrl(\n 'js-client-browser-token',\n )} for more information and how to hide this warning.`,\n])\n\nexport const printCredentialedTokenWarning = createWarningPrinter([\n 'You have configured Sanity client to use a token, but also provided `withCredentials: true`.',\n 'This is no longer supported - only token will be used - remove `withCredentials: true`.',\n])\n\nexport const printNoApiVersionSpecifiedWarning = createWarningPrinter([\n 'Using the Sanity client without specifying an API version is deprecated.',\n `See ${generateHelpUrl('js-client-api-version')}`,\n])\n\nexport const printNoDefaultExport = createWarningPrinter([\n 'The default export of @sanity/client has been deprecated. Use the named export `createClient` instead.',\n])\n","import {generateHelpUrl} from './generateHelpUrl'\nimport type {ClientConfig, ClientPerspective, InitializedClientConfig} from './types'\nimport * as validate from './validators'\nimport * as warnings from './warnings'\n\nconst defaultCdnHost = 'apicdn.sanity.io'\nexport const defaultConfig = {\n apiHost: 'https://api.sanity.io',\n apiVersion: '1',\n useProjectHostname: true,\n stega: {enabled: false},\n} satisfies ClientConfig\n\nconst LOCALHOSTS = ['localhost', '127.0.0.1', '0.0.0.0']\nconst isLocal = (host: string) => LOCALHOSTS.indexOf(host) !== -1\n\nfunction validateApiVersion(apiVersion: string) {\n if (apiVersion === '1' || apiVersion === 'X') {\n return\n }\n\n const apiDate = new Date(apiVersion)\n const apiVersionValid =\n /^\\d{4}-\\d{2}-\\d{2}$/.test(apiVersion) && apiDate instanceof Date && apiDate.getTime() > 0\n\n if (!apiVersionValid) {\n throw new Error('Invalid API version string, expected `1` or date in format `YYYY-MM-DD`')\n }\n}\n\n/**\n * @internal - it may have breaking changes in any release\n */\nexport function validateApiPerspective(\n perspective: unknown,\n): asserts perspective is ClientPerspective {\n if (Array.isArray(perspective) && perspective.length > 1 && perspective.includes('raw')) {\n throw new TypeError(\n `Invalid API perspective value: \"raw\". The raw-perspective can not be combined with other perspectives`,\n )\n }\n}\n\nexport const initConfig = (\n config: Partial<ClientConfig>,\n prevConfig: Partial<ClientConfig>,\n): InitializedClientConfig => {\n const specifiedConfig = {\n ...prevConfig,\n ...config,\n stega: {\n ...(typeof prevConfig.stega === 'boolean'\n ? {enabled: prevConfig.stega}\n : prevConfig.stega || defaultConfig.stega),\n ...(typeof config.stega === 'boolean' ? {enabled: config.stega} : config.stega || {}),\n },\n }\n if (!specifiedConfig.apiVersion) {\n warnings.printNoApiVersionSpecifiedWarning()\n }\n\n const newConfig = {\n ...defaultConfig,\n ...specifiedConfig,\n } as InitializedClientConfig\n const projectBased = newConfig.useProjectHostname && !newConfig['~experimental_resource']\n\n if (typeof Promise === 'undefined') {\n const helpUrl = generateHelpUrl('js-client-promise-polyfill')\n throw new Error(`No native Promise-implementation found, polyfill needed - see ${helpUrl}`)\n }\n\n if (projectBased && !newConfig.projectId) {\n throw new Error('Configuration must contain `projectId`')\n }\n\n if (newConfig['~experimental_resource']) {\n validate.resourceConfig(newConfig)\n }\n\n if (typeof newConfig.perspective !== 'undefined') {\n validateApiPerspective(newConfig.perspective)\n }\n\n if ('encodeSourceMap' in newConfig) {\n throw new Error(\n `It looks like you're using options meant for '@sanity/preview-kit/client'. 'encodeSourceMap' is not supported in '@sanity/client'. Did you mean 'stega.enabled'?`,\n )\n }\n if ('encodeSourceMapAtPath' in newConfig) {\n throw new Error(\n `It looks like you're using options meant for '@sanity/preview-kit/client'. 'encodeSourceMapAtPath' is not supported in '@sanity/client'. Did you mean 'stega.filter'?`,\n )\n }\n if (typeof newConfig.stega.enabled !== 'boolean') {\n throw new Error(`stega.enabled must be a boolean, received ${newConfig.stega.enabled}`)\n }\n if (newConfig.stega.enabled && newConfig.stega.studioUrl === undefined) {\n throw new Error(`stega.studioUrl must be defined when stega.enabled is true`)\n }\n if (\n newConfig.stega.enabled &&\n typeof newConfig.stega.studioUrl !== 'string' &&\n typeof newConfig.stega.studioUrl !== 'function'\n ) {\n throw new Error(\n `stega.studioUrl must be a string or a function, received ${newConfig.stega.studioUrl}`,\n )\n }\n\n const isBrowser = typeof window !== 'undefined' && window.location && window.location.hostname\n const isLocalhost = isBrowser && isLocal(window.location.hostname)\n\n const hasToken = Boolean(newConfig.token)\n if (newConfig.withCredentials && hasToken) {\n warnings.printCredentialedTokenWarning()\n newConfig.withCredentials = false\n }\n\n if (isBrowser && isLocalhost && hasToken && newConfig.ignoreBrowserTokenWarning !== true) {\n warnings.printBrowserTokenWarning()\n } else if (typeof newConfig.useCdn === 'undefined') {\n warnings.printCdnWarning()\n }\n\n if (projectBased) {\n validate.projectId(newConfig.projectId!)\n }\n\n if (newConfig.dataset) {\n validate.dataset(newConfig.dataset)\n }\n\n if ('requestTagPrefix' in newConfig) {\n // Allow setting and unsetting request tag prefix\n newConfig.requestTagPrefix = newConfig.requestTagPrefix\n ? validate.requestTag(newConfig.requestTagPrefix).replace(/\\.+$/, '')\n : undefined\n }\n\n newConfig.apiVersion = `${newConfig.apiVersion}`.replace(/^v/, '')\n newConfig.isDefaultApi = newConfig.apiHost === defaultConfig.apiHost\n\n if (newConfig.useCdn === true && newConfig.withCredentials) {\n warnings.printCdnAndWithCredentialsWarning()\n }\n\n // If `useCdn` is undefined, we treat it as `true`\n newConfig.useCdn = newConfig.useCdn !== false && !newConfig.withCredentials\n\n validateApiVersion(newConfig.apiVersion)\n\n const hostParts = newConfig.apiHost.split('://', 2)\n const protocol = hostParts[0]\n const host = hostParts[1]\n const cdnHost = newConfig.isDefaultApi ? defaultCdnHost : host\n\n if (projectBased) {\n newConfig.url = `${protocol}://${newConfig.projectId}.${host}/v${newConfig.apiVersion}`\n newConfig.cdnUrl = `${protocol}://${newConfig.projectId}.${cdnHost}/v${newConfig.apiVersion}`\n } else {\n newConfig.url = `${newConfig.apiHost}/v${newConfig.apiVersion}`\n newConfig.cdnUrl = newConfig.url\n }\n\n return newConfig\n}\n","import {defer, isObservable, mergeMap, Observable, of} from 'rxjs'\n\nimport {formatQueryParseError, isQueryParseError} from '../http/errors'\nimport {type Any} from '../types'\n\n/**\n * @public\n * Thrown if the EventSource connection could not be established.\n * Note that ConnectionFailedErrors are rare, and disconnects will normally be handled by the EventSource instance itself and emitted as `reconnect` events.\n */\nexport class ConnectionFailedError extends Error {\n readonly name = 'ConnectionFailedError'\n}\n\n/**\n * The listener has been told to explicitly disconnect.\n * This is a rare situation, but may occur if the API knows reconnect attempts will fail,\n * eg in the case of a deleted dataset, a blocked project or similar events.\n * @public\n */\nexport class DisconnectError extends Error {\n readonly name = 'DisconnectError'\n readonly reason?: string\n constructor(message: string, reason?: string, options: ErrorOptions = {}) {\n super(message, options)\n this.reason = reason\n }\n}\n\n/**\n * @public\n * The server sent a `channelError` message. Usually indicative of a bad or malformed request\n */\nexport class ChannelError extends Error {\n readonly name = 'ChannelError'\n readonly data?: unknown\n constructor(message: string, data: unknown) {\n super(message)\n this.data = data\n }\n}\n\n/**\n * @public\n * The server sent an `error`-event to tell the client that an unexpected error has happened.\n */\nexport class MessageError extends Error {\n readonly name = 'MessageError'\n readonly data?: unknown\n constructor(message: string, data: unknown, options: ErrorOptions = {}) {\n super(message, options)\n this.data = data\n }\n}\n\n/**\n * @public\n * An error occurred while parsing the message sent by the server as JSON. Should normally not happen.\n */\nexport class MessageParseError extends Error {\n readonly name = 'MessageParseError'\n}\n\n/**\n * @public\n */\nexport interface ServerSentEvent<Name extends string> {\n type: Name\n id?: string\n data?: unknown\n}\n\n// Always listen for these events, no matter what\nconst REQUIRED_EVENTS = ['channelError', 'disconnect']\n\n/**\n * @internal\n */\nexport type EventSourceEvent<Name extends string> = ServerSentEvent<Name>\n\n/**\n * @internal\n */\nexport type EventSourceInstance = InstanceType<typeof globalThis.EventSource>\n\n/**\n * Sanity API specific EventSource handler shared between the listen and live APIs\n *\n * Since the `EventSource` API is not provided by all environments, this function enables custom initialization of the EventSource instance\n * for runtimes that requires polyfilling or custom setup logic (e.g. custom HTTP headers)\n * via the passed `initEventSource` function which must return an EventSource instance.\n *\n * Possible errors to be thrown on the returned observable are:\n * - {@link MessageError}\n * - {@link MessageParseError}\n * - {@link ChannelError}\n * - {@link DisconnectError}\n * - {@link ConnectionFailedError}\n *\n * @param initEventSource - A function that returns an EventSource instance or an Observable that resolves to an EventSource instance\n * @param events - an array of named events from the API to listen for.\n *\n * @internal\n */\nexport function connectEventSource<EventName extends string>(\n initEventSource: () => EventSourceInstance | Observable<EventSourceInstance>,\n events: EventName[],\n) {\n return defer(() => {\n const es = initEventSource()\n return isObservable(es) ? es : of(es)\n }).pipe(mergeMap((es) => connectWithESInstance(es, events))) as Observable<\n ServerSentEvent<EventName>\n >\n}\n\n/**\n * Provides an observable from the passed EventSource instance, subscribing to the passed list of names of events types to listen for\n * Handles connection logic, adding/removing event listeners, payload parsing, error propagation, etc.\n *\n * @param es - The EventSource instance\n * @param events - List of event names to listen for\n */\nfunction connectWithESInstance<EventTypeName extends string>(\n es: EventSourceInstance,\n events: EventTypeName[],\n) {\n return new Observable<EventSourceEvent<EventTypeName>>((observer) => {\n const emitOpen = (events as string[]).includes('open')\n const emitReconnect = (events as string[]).includes('reconnect')\n\n // EventSource will emit a regular Event if it fails to connect, however the API may also emit an `error` MessageEvent\n // So we need to handle both cases\n function onError(evt: MessageEvent | Event) {\n // If the event has a `data` property, then it`s a MessageEvent emitted by the API and we should forward the error\n if ('data' in evt) {\n const [parseError, event] = parseEvent(evt as MessageEvent)\n observer.error(\n parseError\n ? new MessageParseError('Unable to parse EventSource error message', {cause: event})\n : new MessageError((event?.data as {message: string}).message, event),\n )\n return\n }\n\n // We should never be in a disconnected state. By default, EventSource will reconnect\n // automatically, but in some cases (like when a laptop lid is closed), it will trigger onError\n // if it can't reconnect.\n // see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model\n if (es.readyState === es.CLOSED) {\n // In these cases we'll signal to consumers (via the error path) that a retry/reconnect is needed.\n observer.error(new ConnectionFailedError('EventSource connection failed'))\n } else if (emitReconnect) {\n observer.next({type: 'reconnect' as EventTypeName})\n }\n }\n\n function onOpen() {\n // The open event of the EventSource API is fired when a connection with an event source is opened.\n observer.next({type: 'open' as EventTypeName})\n }\n\n function onMessage(message: MessageEvent) {\n const [parseError, event] = parseEvent(message)\n if (parseError) {\n observer.error(\n new MessageParseError('Unable to parse EventSource message', {cause: parseError}),\n )\n return\n }\n if (message.type === 'channelError') {\n // An error occurred. This is different from a network-level error (which will be emitted as 'error').\n // Possible causes are things such as malformed filters, non-existant datasets\n // or similar.\n const tag = new URL(es.url).searchParams.get('tag')\n observer.error(new ChannelError(extractErrorMessage(event?.data, tag), event.data))\n return\n }\n if (message.type === 'disconnect') {\n // The listener has been told to explicitly disconnect and not reconnect.\n // This is a rare situation, but may occur if the API knows reconnect attempts will fail,\n // eg in the case of a deleted dataset, a blocked project or similar events.\n observer.error(\n new DisconnectError(\n `Server disconnected client: ${\n (event.data as {reason?: string})?.reason || 'unknown error'\n }`,\n ),\n )\n return\n }\n observer.next({\n type: message.type as EventTypeName,\n id: message.lastEventId,\n ...(event.data ? {data: event.data} : {}),\n })\n }\n\n es.addEventListener('error', onError)\n\n if (emitOpen) {\n es.addEventListener('open', onOpen)\n }\n\n // Make sure we have a unique list of events types to avoid listening multiple times,\n const cleanedEvents = [...new Set([...REQUIRED_EVENTS, ...events])]\n // filter out events that are handled separately\n .filter((type) => type !== 'error' && type !== 'open' && type !== 'reconnect')\n\n cleanedEvents.forEach((type: string) => es.addEventListener(type, onMessage))\n\n return () => {\n es.removeEventListener('error', onError)\n if (emitOpen) {\n es.removeEventListener('open', onOpen)\n }\n cleanedEvents.forEach((type: string) => es.removeEventListener(type, onMessage))\n es.close()\n }\n })\n}\n\nfunction parseEvent(\n message: MessageEvent,\n): [null, {type: string; id: string; data?: unknown}] | [Error, null] {\n try {\n const data = typeof message.data === 'string' && JSON.parse(message.data)\n return [\n null,\n {\n type: message.type,\n id: message.lastEventId,\n ...(isEmptyObject(data) ? {} : {data}),\n },\n ]\n } catch (err) {\n return [err as Error, null]\n }\n}\n\nfunction extractErrorMessage(err: Any, tag?: string | null) {\n const error = err.error\n\n if (!error) {\n return err.message || 'Unknown listener error'\n }\n\n if (isQueryParseError(error)) {\n return formatQueryParseError(error, tag)\n }\n\n if (error.description) {\n return error.description\n }\n\n return typeof error === 'string' ? error : JSON.stringify(error, null, 2)\n}\n\nfunction isEmptyObject(data: object) {\n for (const _ in data) {\n return false\n }\n return true\n}\n","import type {MutationSelection} from '../types'\n\nexport function getSelection(sel: unknown): MutationSelection {\n if (typeof sel === 'string') {\n return {id: sel}\n }\n\n if (Array.isArray(sel)) {\n return {query: '*[_id in $ids]', params: {ids: sel}}\n }\n\n if (typeof sel === 'object' && sel !== null && 'query' in sel && typeof sel.query === 'string') {\n return 'params' in sel && typeof sel.params === 'object' && sel.params !== null\n ? {query: sel.query, params: sel.params}\n : {query: sel.query}\n }\n\n const selectionOpts = [\n '* Document ID (<docId>)',\n '* Array of document IDs',\n '* Object containing `query`',\n ].join('\\n')\n\n throw new Error(`Unknown selection - must be one of:\\n\\n${selectionOpts}`)\n}\n","import {type Observable} from 'rxjs'\n\nimport type {ObservableSanityClient, SanityClient} from '../SanityClient'\nimport type {\n AllDocumentIdsMutationOptions,\n AllDocumentsMutationOptions,\n Any,\n AttributeSet,\n BaseMutationOptions,\n FirstDocumentIdMutationOptions,\n FirstDocumentMutationOptions,\n MultipleMutationResult,\n PatchMutationOperation,\n PatchOperations,\n PatchSelection,\n SanityDocument,\n SingleMutationResult,\n} from '../types'\nimport {getSelection} from '../util/getSelection'\nimport {validateInsert, validateObject} from '../validators'\n\n/** @internal */\nexport class BasePatch {\n protected selection: PatchSelection\n protected operations: PatchOperations\n constructor(selection: PatchSelection, operations: PatchOperations = {}) {\n this.selection = selection\n this.operations = operations\n }\n\n /**\n * Sets the given attributes to the document. Does NOT merge objects.\n * The operation is added to the current patch, ready to be commited by `commit()`\n *\n * @param attrs - Attributes to set. To set a deep attribute, use JSONMatch, eg: \\{\"nested.prop\": \"value\"\\}\n */\n set(attrs: AttributeSet): this {\n return this._assign('set', attrs)\n }\n\n /**\n * Sets the given attributes to the document if they are not currently set. Does NOT merge objects.\n * The operation is added to the current patch, ready to be commited by `commit()`\n *\n * @param attrs - Attributes to set. To set a deep attribute, use JSONMatch, eg: \\{\"nested.prop\": \"value\"\\}\n */\n setIfMissing(attrs: AttributeSet): this {\n return this._assign('setIfMissing', attrs)\n }\n\n /**\n * Performs a \"diff-match-patch\" operation on the string attributes provided.\n * The operation is added to the current patch, ready to be commited by `commit()`\n *\n * @param attrs - Attributes to perform operation on. To set a deep attribute, use JSONMatch, eg: \\{\"nested.prop\": \"dmp\"\\}\n */\n diffMatchPatch(attrs: AttributeSet): this {\n validateObject('diffMatchPatch', attrs)\n return this._assign('diffMatchPatch', attrs)\n }\n\n /**\n * Unsets the attribute paths provided.\n * The operation is added to the current patch, ready to be commited by `commit()`\n *\n * @param attrs - Attribute paths to unset.\n */\n unset(attrs: string[]): this {\n if (!Array.isArray(attrs)) {\n throw new Error('unset(attrs) takes an array of attributes to unset, non-array given')\n }\n\n this.operations = Object.assign({}, this.operations, {unset: attrs})\n return this\n }\n\n /**\n * Increment a numeric value. Each entry in the argument is either an attribute or a JSON path. The value may be a positive or negative integer or floating-point value. The operation will fail if target value is not a numeric value, or doesn't exist.\n *\n * @param attrs - Object of attribute paths to increment, values representing the number to increment by.\n */\n inc(attrs: {[key: string]: number}): this {\n return this._assign('inc', attrs)\n }\n\n /**\n * Decrement a numeric value. Each entry in the argument is either an attribute or a JSON path. The value may be a positive or negative integer or floating-point value. The operation will fail if target value is not a numeric value, or doesn't exist.\n *\n * @param attrs - Object of attribute paths to decrement, values representing the number to decrement by.\n */\n dec(attrs: {[key: string]: number}): this {\n return this._assign('dec', attrs)\n }\n\n /**\n * Provides methods for modifying arrays, by inserting, appending and replacing elements via a JSONPath expression.\n *\n * @param at - Location to insert at, relative to the given selector, or 'replace' the matched path\n * @param selector - JSONPath expression, eg `comments[-1]` or `blocks[_key==\"abc123\"]`\n * @param items - Array of items to insert/replace\n */\n insert(at: 'before' | 'after' | 'replace', selector: string, items: Any[]): this {\n validateInsert(at, selector, items)\n return this._assign('insert', {[at]: selector, items})\n }\n\n /**\n * Append the given items to the array at the given JSONPath\n *\n * @param selector - Attribute/path to append to, eg `comments` or `person.hobbies`\n * @param items - Array of items to append to the array\n */\n append(selector: string, items: Any[]): this {\n return this.insert('after', `${selector}[-1]`, items)\n }\n\n /**\n * Prepend the given items to the array at the given JSONPath\n *\n * @param selector - Attribute/path to prepend to, eg `comments` or `person.hobbies`\n * @param items - Array of items to prepend to the array\n */\n prepend(selector: string, items: Any[]): this {\n return this.insert('before', `${selector}[0]`, items)\n }\n\n /**\n * Change the contents of an array by removing existing elements and/or adding new elements.\n *\n * @param selector - Attribute or JSONPath expression for array\n * @param start - Index at which to start changing the array (with origin 0). If greater than the length of the array, actual starting index will be set to the length of the array. If negative, will begin that many elements from the end of the array (with origin -1) and will be set to 0 if absolute value is greater than the length of the array.x\n * @param deleteCount - An integer indicating the number of old array elements to remove.\n * @param items - The elements to add to the array, beginning at the start index. If you don't specify any elements, splice() will only remove elements from the array.\n */\n splice(selector: string, start: number, deleteCount?: number, items?: Any[]): this {\n // Negative indexes doesn't mean the same in Sanity as they do in JS;\n // -1 means \"actually at the end of the array\", which allows inserting\n // at the end of the array without knowing its length. We therefore have\n // to substract negative indexes by one to match JS. If you want Sanity-\n // behaviour, just use `insert('replace', selector, items)` directly\n const delAll = typeof deleteCount === 'undefined' || deleteCount === -1\n const startIndex = start < 0 ? start - 1 : start\n const delCount = delAll ? -1 : Math.max(0, start + deleteCount)\n const delRange = startIndex < 0 && delCount >= 0 ? '' : delCount\n const rangeSelector = `${selector}[${startIndex}:${delRange}]`\n return this.insert('replace', rangeSelector, items || [])\n }\n\n /**\n * Adds a revision clause, preventing the document from being patched if the `_rev` property does not match the given value\n *\n * @param rev - Revision to lock the patch to\n */\n ifRevisionId(rev: string): this {\n this.operations.ifRevisionID = rev\n return this\n }\n\n /**\n * Return a plain JSON representation of the patch\n */\n serialize(): PatchMutationOperation {\n return {...getSelection(this.selection), ...this.operations}\n }\n\n /**\n * Return a plain JSON representation of the patch\n */\n toJSON(): PatchMutationOperation {\n return this.serialize()\n }\n\n /**\n * Clears the patch of all operations\n */\n reset(): this {\n this.operations = {}\n return this\n }\n\n protected _assign(op: keyof PatchOperations, props: Any, merge = true): this {\n validateObject(op, props)\n this.operations = Object.assign({}, this.operations, {\n [op]: Object.assign({}, (merge && this.operations[op]) || {}, props),\n })\n return this\n }\n\n protected _set(op: keyof PatchOperations, props: Any): this {\n return this._assign(op, props, false)\n }\n}\n\n/** @public */\nexport class ObservablePatch extends BasePatch {\n #client?: ObservableSanityClient\n\n constructor(\n selection: PatchSelection,\n operations?: PatchOperations,\n client?: ObservableSanityClient,\n ) {\n super(selection, operations)\n this.#client = client\n }\n\n /**\n * Clones the patch\n */\n clone(): ObservablePatch {\n return new ObservablePatch(this.selection, {...this.operations}, this.#client)\n }\n\n /**\n * Commit the patch, returning an observable that produces the first patched document\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options: FirstDocumentMutationOptions,\n ): Observable<SanityDocument<R>>\n /**\n * Commit the patch, returning an observable that produces an array of the mutated documents\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options: AllDocumentsMutationOptions,\n ): Observable<SanityDocument<R>[]>\n /**\n * Commit the patch, returning an observable that produces a mutation result object\n *\n * @param options - Options for the mutation operation\n */\n commit(options: FirstDocumentIdMutationOptions): Observable<SingleMutationResult>\n /**\n * Commit the patch, returning an observable that produces a mutation result object\n *\n * @param options - Options for the mutation operation\n */\n commit(options: AllDocumentIdsMutationOptions): Observable<MultipleMutationResult>\n /**\n * Commit the patch, returning an observable that produces the first patched document\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options?: BaseMutationOptions,\n ): Observable<SanityDocument<R>>\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options?:\n | FirstDocumentMutationOptions\n | AllDocumentsMutationOptions\n | FirstDocumentIdMutationOptions\n | AllDocumentIdsMutationOptions\n | BaseMutationOptions,\n ): Observable<\n SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult\n > {\n if (!this.#client) {\n throw new Error(\n 'No `client` passed to patch, either provide one or pass the ' +\n 'patch to a clients `mutate()` method',\n )\n }\n\n const returnFirst = typeof this.selection === 'string'\n const opts = Object.assign({returnFirst, returnDocuments: true}, options)\n return this.#client.mutate<R>({patch: this.serialize()} as Any, opts)\n }\n}\n\n/** @public */\nexport class Patch extends BasePatch {\n #client?: SanityClient\n constructor(selection: PatchSelection, operations?: PatchOperations, client?: SanityClient) {\n super(selection, operations)\n this.#client = client\n }\n\n /**\n * Clones the patch\n */\n clone(): Patch {\n return new Patch(this.selection, {...this.operations}, this.#client)\n }\n\n /**\n * Commit the patch, returning a promise that resolves to the first patched document\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options: FirstDocumentMutationOptions,\n ): Promise<SanityDocument<R>>\n /**\n * Commit the patch, returning a promise that resolves to an array of the mutated documents\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options: AllDocumentsMutationOptions,\n ): Promise<SanityDocument<R>[]>\n /**\n * Commit the patch, returning a promise that resolves to a mutation result object\n *\n * @param options - Options for the mutation operation\n */\n commit(options: FirstDocumentIdMutationOptions): Promise<SingleMutationResult>\n /**\n * Commit the patch, returning a promise that resolves to a mutation result object\n *\n * @param options - Options for the mutation operation\n */\n commit(options: AllDocumentIdsMutationOptions): Promise<MultipleMutationResult>\n /**\n * Commit the patch, returning a promise that resolves to the first patched document\n *\n * @param options - Options for the mutation operation\n */\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options?: BaseMutationOptions,\n ): Promise<SanityDocument<R>>\n commit<R extends Record<string, Any> = Record<string, Any>>(\n options?:\n | FirstDocumentMutationOptions\n | AllDocumentsMutationOptions\n | FirstDocumentIdMutationOpt