UNPKG

@sanity/client

Version:

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

373 lines (372 loc) 15.9 kB
import { t as isRecord } from "./isRecord-Kfmt-nk-.js"; import { createRequester, isHttpError } from "get-it"; import { isRetryableRequest, retry } from "get-it/middleware"; import { Observable, from } from "rxjs"; /** * RegExp to test for newlines. */ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/; /** * Highlight a code frame with the given location and message. * * @param query - The query to be highlighted. * @param location - The location of the error in the code/query. * @param message - Message to be displayed inline (if possible) next to the highlighted * location in the code. If it can't be positioned inline, it will be placed above the * code frame. * @returns The highlighted code frame. */ function codeFrame(query, location, message) { let lines = query.split(NEWLINE), { start, end, markerLines } = getMarkerLines({ start: columnToLine(location.start, lines), end: location.end ? columnToLine(location.end, lines) : void 0 }, lines), numberMaxWidth = `${end}`.length; return query.split(NEWLINE, end).slice(start, end).map((line, index) => { let number = start + 1 + index, gutter = ` ${` ${number}`.slice(-numberMaxWidth)} |`, hasMarker = markerLines[number], lastMarkerLine = !markerLines[number + 1]; if (!hasMarker) return ` ${gutter}${line.length > 0 ? ` ${line}` : ""}`; let markerLine = ""; if (Array.isArray(hasMarker)) { let markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " "), numberOfMarkers = hasMarker[1] || 1; markerLine = [ "\n ", gutter.replace(/\d/g, " "), " ", markerSpacing, "^".repeat(numberOfMarkers) ].join(""), lastMarkerLine && message && (markerLine += " " + message); } return [ ">", gutter, line.length > 0 ? ` ${line}` : "", markerLine ].join(""); }).join("\n"); } function getMarkerLines(loc, source) { let startLoc = { ...loc.start }, endLoc = { ...startLoc, ...loc.end }, startLine = startLoc.line ?? -1, startColumn = startLoc.column ?? 0, endLine = endLoc.line, endColumn = endLoc.column, start = Math.max(startLine - 3, 0), end = Math.min(source.length, endLine + 3); startLine === -1 && (start = 0), endLine === -1 && (end = source.length); let lineDiff = endLine - startLine, markerLines = {}; if (lineDiff) for (let i = 0; i <= lineDiff; i++) { let lineNumber = i + startLine; markerLines[lineNumber] = startColumn ? i === 0 ? [startColumn, source[lineNumber - 1].length - startColumn + 1] : i === lineDiff ? [0, endColumn] : [0, source[lineNumber - i].length] : !0; } else markerLines[startLine] = startColumn === endColumn ? !startColumn || [startColumn, 0] : [startColumn, endColumn - startColumn]; return { start, end, markerLines }; } function columnToLine(column, lines) { let offset = 0; for (let i = 0; i < lines.length; i++) { let lineLength = lines[i].length + 1; if (offset + lineLength > column) return { line: i + 1, column: column - offset }; offset += lineLength; } return { line: lines.length, column: lines[lines.length - 1]?.length ?? 0 }; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(o) { return typeof o; } : function(o) { return o && typeof Symbol == "function" && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function toPrimitive(t, r) { if (_typeof(t) != "object" || !t) return t; var e = t[Symbol.toPrimitive]; if (e !== void 0) { var i = e.call(t, r || "default"); if (_typeof(i) != "object") return i; throw TypeError("@@toPrimitive must return a primitive value."); } return (r === "string" ? String : Number)(t); } function toPropertyKey(t) { var i = toPrimitive(t, "string"); return _typeof(i) == "symbol" ? i : i + ""; } function _defineProperty(e, r, t) { return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } /** * Adapter for buffered responses from get-it v9 (`BufferedResponse`-shaped). * * The URL and method aren't on the response itself in v9, so the request * options must be passed alongside. * * @internal */ function httpResponseFromFetch(res, reqUrl, reqMethod) { return { statusCode: res.status, statusMessage: res.statusText || null, headers: headersToRecord$1(res.headers), body: res.body, url: res.url || reqUrl, method: reqMethod }; } function headersToRecord$1(headers) { let out = {}; return headers.forEach((value, key) => { out[key] = value; }), out; } /** * Checks if the provided error is an HTTP error. * * @param error - The error to check. * @returns `true` if the error is an HTTP error, `false` otherwise. * @public */ function isHttpError$1(error) { if (!isRecord(error)) return !1; let response = error.response; return !(typeof error.statusCode != "number" || typeof error.message != "string" || !isRecord(response) || response.body === void 0 || typeof response.url != "string" || typeof response.method != "string" || typeof response.headers != "object" || typeof response.statusCode != "number"); } /** @public */ var ClientError = class extends Error { constructor(res, tag) { let props = extractErrorProps(res, tag); super(props.message), _defineProperty(this, "response", void 0), _defineProperty(this, "statusCode", 400), _defineProperty(this, "responseBody", void 0), _defineProperty(this, "traceId", void 0), _defineProperty(this, "details", void 0), Object.assign(this, props); } }, ServerError = class extends Error { constructor(res) { let props = extractErrorProps(res); super(props.message), _defineProperty(this, "response", void 0), _defineProperty(this, "statusCode", 500), _defineProperty(this, "responseBody", void 0), _defineProperty(this, "traceId", void 0), _defineProperty(this, "details", void 0), Object.assign(this, props); } }; function extractErrorProps(res, tag) { let body = res.body, props = { response: res, statusCode: res.statusCode, responseBody: stringifyBody(body, res), traceId: extractTraceId(res), message: "", details: void 0 }; if (!isRecord(body)) return props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props; let error = body.error; if (typeof error == "string" && typeof body.message == "string") return props.message = `${error} - ${body.message}${formatTraceId(props.traceId)}`, props; if (typeof error != "object" || !error) return props.message = typeof error == "string" ? `${error}${formatTraceId(props.traceId)}` : typeof body.message == "string" ? `${body.message}${formatTraceId(props.traceId)}` : `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props; if (isMutationError(error) || isActionError(error)) { let allItems = error.items || [], items = allItems.slice(0, 5).map((item) => item.error?.description).filter(Boolean), itemsStr = items.length ? `:\n- ${items.join("\n- ")}` : ""; return allItems.length > 5 && (itemsStr += `\n...and ${allItems.length - 5} more`), props.message = `${error.description}${formatTraceId(props.traceId)}${itemsStr}`, props.details = body.error, props; } return isQueryParseError(error) ? (props.message = formatQueryParseError(error, tag, props.traceId), props.details = body.error, props) : "description" in error && typeof error.description == "string" ? (props.message = `${error.description}${formatTraceId(props.traceId)}`, props.details = error, props) : (props.message = `${httpErrorMessage(res, body)}${formatTraceId(props.traceId)}`, props); } function isMutationError(error) { return "type" in error && error.type === "mutationError" && "description" in error && typeof error.description == "string"; } function isActionError(error) { return "type" in error && error.type === "actionError" && "description" in error && typeof error.description == "string"; } /** @internal */ function isQueryParseError(error) { return isRecord(error) && error.type === "queryParseError" && typeof error.query == "string" && typeof error.start == "number" && typeof error.end == "number"; } /** * Formats a GROQ query parse error into a human-readable string. * * @param error - The error object containing details about the parse error. * @param tag - An optional tag to include in the error message. * @returns A formatted error message string. * @public */ function formatQueryParseError(error, tag, traceId) { let { query, start, end, description } = error, withTraceId = traceId ? `\n(traceId: ${traceId})` : ""; if (!query || start === void 0) return `GROQ query parse error: ${description}${withTraceId}`; let withTag = tag ? `\n\nTag: ${tag}` : ""; return `GROQ query parse error:\n${codeFrame(query, { start, end }, description)}${withTag}${withTraceId}`; } function httpErrorMessage(res, body) { let details = typeof body == "string" ? ` (${sliceWithEllipsis(body, 100)})` : "", statusMessage = res.statusMessage ? ` ${res.statusMessage}` : ""; return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`; } /** * Extract the traceId from the traceparent header on the response. * * The traceparent is on the format [version]-[traceId]-[parentId]-[traceFlags], but * when debugging end-user issues it's the traceId we need to be able to get hold of * the relevant traces. * * @see https://www.w3.org/TR/trace-context/ * @returns The traceId for HTTP response */ function extractTraceId(res) { let traceparent = res?.headers?.traceparent; if (traceparent) return traceparent.split("-")[1]; } function stringifyBody(body, res) { return (res.headers["content-type"] || "").toLowerCase().indexOf("application/json") === -1 ? body : JSON.stringify(body, null, 2); } function formatTraceId(traceId) { return traceId ? ` (traceId: ${traceId})` : ""; } function sliceWithEllipsis(str, max) { return str.length > max ? `${str.slice(0, max)}…` : str; } /** @public */ var CorsOriginError = class extends Error { constructor({ projectId, credentials } = {}) { if (super("CorsOriginError"), _defineProperty(this, "projectId", void 0), _defineProperty(this, "addOriginUrl", void 0), this.name = "CorsOriginError", this.projectId = projectId, projectId && typeof location < "u") { let url = new URL(`https://sanity.io/manage/project/${projectId}/api`), { origin } = location; url.searchParams.set("cors", "add"), url.searchParams.set("origin", origin), credentials && url.searchParams.set("credentials", ""), this.addOriginUrl = url, this.message = `The current origin is not allowed to connect to the Live Content API. Add it here: ${url}`; } else this.message = projectId ? `The current origin is not allowed to connect to the Live Content API. Change your configuration here: https://sanity.io/manage/project/${projectId}/api` : "The current origin is not allowed to connect to the Live Content API."; } }; /** * Build both the observable and promise transport forms from a single get-it * requester. The promise form is the primitive (`executeRequest` is already * promise-based); the observable form wraps it lazily so each subscription * starts its own request (cold), and unsubscribing aborts the in-flight * fetch — the same contract as the get-it v8 observable adapter. * * @internal */ function defineRequester(envOptions, config = {}) { let applyFetchInit = (opts, next) => { let fetchInit = opts.meta?.fetchInit; if (typeof fetchInit != "object" || !fetchInit) return next(opts); let baseFetch = opts.fetch ?? envOptions.fetch ?? globalThis.fetch, fetchWithInit = (input, init) => baseFetch(input, { ...fetchInit, ...init }); return next({ ...opts, fetch: fetchWithInit }); }, requester = createRequester({ ...envOptions.fetch ? { fetch: envOptions.fetch } : {}, headers: envOptions.headers, httpErrors: !0, middleware: [ retry({ shouldRetry: shouldRetryRequest, maxRetries: config.maxRetries ?? 5, ...config.retryDelay ? { retryDelay: config.retryDelay } : {} }), ...envOptions.middleware, applyFetchInit, printWarnings(config) ] }), promise = (options) => { if (typeof options.url != "string") throw TypeError("Request options must include a `url`"); return executeRequest(requester, options); }, observable = (options) => new Observable((subscriber) => { let controller = new AbortController(), userSignal = options.signal, signal = userSignal ? AbortSignal.any([userSignal, controller.signal]) : controller.signal, subscription = from(promise({ ...options, signal })).subscribe(subscriber); return () => { subscription.unsubscribe(), controller.abort(); }; }); return { promise, observable }; } async function executeRequest(requester, fetchOptions) { let url = fetchOptions.url, method = (fetchOptions.method ?? "GET").toUpperCase(), response; try { response = await requester(fetchOptions); } catch (err) { if (isHttpError(err)) { let errBody = parseJsonText(typeof err.body == "string" ? err.body : "", err.headers), canonical = httpResponseFromFetch({ status: err.status, statusText: err.statusText, headers: err.headers, body: errBody, url: err.response.url ?? err.url }, url, method), tag = extractRequestTag(fetchOptions.query); throw canonical.statusCode >= 500 ? new ServerError(canonical) : new ClientError(canonical, tag); } throw err; } return { type: "response", body: parseJsonBody(response), statusCode: response.status, statusMessage: response.statusText || null, headers: headersToRecord(response.headers), url: response.url || url, method }; } /** * Extract the GROQ request tag (used for error messages) from the query. */ function extractRequestTag(query) { if (!query) return; if (query instanceof URLSearchParams) return query.get("tag") ?? void 0; let tag = query.tag; return typeof tag == "string" ? tag : void 0; } function parseJsonBody(response) { return parseJsonText(response.text(), response.headers); } /** * Parse a response body according to its `content-type`: JSON when the header * says so (falling back to the raw text on malformed JSON), text otherwise. * Shared with the browser XHR upload path so error bodies parse identically * on both transports. * * @internal */ function parseJsonText(text, headers) { let contentType = (headers.get("content-type") ?? "").toLowerCase(); if (text) { if (contentType.includes("application/json")) try { return JSON.parse(text); } catch { return text; } return text; } } function headersToRecord(headers) { let out = {}; return headers.forEach((value, key) => { out[key] = value; }), out; } function shouldRetryRequest(err, attempt, options) { if (isHttpError(err)) { let isSafe = (options.method ?? "GET") === "GET" || options.method === "HEAD", isQuery = (options.url ?? "").includes("/data/query"), status = err.status; return !!((isSafe || isQuery) && (status === 429 || status === 502 || status === 503)); } return isRetryableRequest(err, attempt, options); } function printWarnings(config) { let seen = {}, shouldIgnore = (message) => config.ignoreWarnings !== void 0 && (Array.isArray(config.ignoreWarnings) ? config.ignoreWarnings : [config.ignoreWarnings]).some((pattern) => typeof pattern == "string" ? message.includes(pattern) : pattern.test(message)); return { afterResponse(response) { let header = response.headers.get("x-sanity-warning"); if (!header) return response; for (let msg of header.split(",").map((m) => m.trim())) !msg || seen[msg] || shouldIgnore(msg) || (seen[msg] = !0, console.warn(msg)); return response; } }; } export { ServerError as a, isHttpError$1 as c, CorsOriginError as i, isQueryParseError as l, parseJsonText as n, formatQueryParseError as o, ClientError as r, httpResponseFromFetch as s, defineRequester as t, _defineProperty as u }; //# sourceMappingURL=request-BhMuKj0D.js.map