@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
373 lines (372 loc) • 15.9 kB
JavaScript
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