UNPKG

@sanity/client

Version:

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

444 lines (443 loc) • 15.1 kB
import { isRecord } from "./stegaClean.js"; const reKeySegment = /_key\s*==\s*['"](.*)['"]/; function isKeySegment(segment) { return typeof segment == "string" ? reKeySegment.test(segment.trim()) : typeof segment == "object" && "_key" in segment; } function toString(path) { if (!Array.isArray(path)) throw new Error("Path is not an array"); return path.reduce((target, segment, i) => { const segmentType = typeof segment; if (segmentType === "number") return `${target}[${segment}]`; if (segmentType === "string") return `${target}${i === 0 ? "" : "."}${segment}`; if (isKeySegment(segment) && segment._key) return `${target}[_key=="${segment._key}"]`; if (Array.isArray(segment)) { const [from, to] = segment; return `${target}[${from}:${to}]`; } throw new Error(`Unsupported path segment \`${JSON.stringify(segment)}\``); }, ""); } const ESCAPE = { "\f": "\\f", "\n": "\\n", "\r": "\\r", " ": "\\t", "'": "\\'", "\\": "\\\\" }, UNESCAPE = { "\\f": "\f", "\\n": ` `, "\\r": "\r", "\\t": " ", "\\'": "'", "\\\\": "\\" }; function jsonPath(path) { return `$${path.map((segment) => typeof segment == "string" ? `['${segment.replace(/[\f\n\r\t'\\]/g, (match) => ESCAPE[match])}']` : typeof segment == "number" ? `[${segment}]` : segment._key !== "" ? `[?(@._key=='${segment._key.replace(/['\\]/g, (match) => ESCAPE[match])}')]` : `[${segment._index}]`).join("")}`; } function jsonPathArray(path) { return path.map((segment) => typeof segment == "string" ? `['${segment.replace(/[\f\n\r\t'\\]/g, (match) => ESCAPE[match])}']` : typeof segment == "number" ? `[${segment}]` : segment._key !== "" ? `[?(@._key=='${segment._key.replace(/['\\]/g, (match) => ESCAPE[match])}')]` : `[${segment._index}]`); } function parseJsonPath(path) { const parsed = [], parseRe = /\['(.*?)'\]|\[(\d+)\]|\[\?\(@\._key=='(.*?)'\)\]/g; let match; for (; (match = parseRe.exec(path)) !== null; ) { if (match[1] !== void 0) { const key = match[1].replace(/\\(\\|f|n|r|t|')/g, (m) => UNESCAPE[m]); parsed.push(key); continue; } if (match[2] !== void 0) { parsed.push(parseInt(match[2], 10)); continue; } if (match[3] !== void 0) { const _key = match[3].replace(/\\(\\')/g, (m) => UNESCAPE[m]); parsed.push({ _key, _index: -1 }); continue; } } return parsed; } function jsonPathToStudioPath(path) { return path.map((segment) => { if (typeof segment == "string" || typeof segment == "number") return segment; if (segment._key !== "") return { _key: segment._key }; if (segment._index !== -1) return segment._index; throw new Error(`invalid segment:${JSON.stringify(segment)}`); }); } function jsonPathToMappingPath(path) { return path.map((segment) => { if (typeof segment == "string" || typeof segment == "number") return segment; if (segment._index !== -1) return segment._index; throw new Error(`invalid segment:${JSON.stringify(segment)}`); }); } function resolveMapping(resultPath, csm) { if (!csm?.mappings) return; const resultMappingPath = jsonPath(jsonPathToMappingPath(resultPath)); if (csm.mappings[resultMappingPath] !== void 0) return { mapping: csm.mappings[resultMappingPath], matchedPath: resultMappingPath, pathSuffix: "" }; const resultMappingPathArray = jsonPathArray(jsonPathToMappingPath(resultPath)); for (let i = resultMappingPathArray.length - 1; i >= 0; i--) { const key = `$${resultMappingPathArray.slice(0, i).join("")}`, mappingFound = csm.mappings[key]; if (mappingFound) { const pathSuffix = resultMappingPath.substring(key.length); return { mapping: mappingFound, matchedPath: key, pathSuffix }; } } } function isArray(value) { return value !== null && Array.isArray(value); } function walkMap(value, mappingFn, path = []) { if (isArray(value)) return value.map((v, idx) => { if (isRecord(v)) { const _key = v._key; if (typeof _key == "string") return walkMap(v, mappingFn, path.concat({ _key, _index: idx })); } return walkMap(v, mappingFn, path.concat(idx)); }); if (isRecord(value)) { if (value._type === "block" || value._type === "span") { const result = { ...value }; return value._type === "block" ? result.children = walkMap(value.children, mappingFn, path.concat("children")) : value._type === "span" && (result.text = walkMap(value.text, mappingFn, path.concat("text"))), result; } return Object.fromEntries( Object.entries(value).map(([k, v]) => [k, walkMap(v, mappingFn, path.concat(k))]) ); } return mappingFn(value, path); } function encodeIntoResult(result, csm, encoder) { return walkMap(result, (value, path) => { if (typeof value != "string") return value; const resolveMappingResult = resolveMapping(path, csm); if (!resolveMappingResult) return value; const { mapping, matchedPath } = resolveMappingResult; if (mapping.type !== "value" || mapping.source.type !== "documentValue") return value; const sourceDocument = csm.documents[mapping.source.document], sourcePath = csm.paths[mapping.source.path], matchPathSegments = parseJsonPath(matchedPath), fullSourceSegments = parseJsonPath(sourcePath).concat(path.slice(matchPathSegments.length)); return encoder({ sourcePath: fullSourceSegments, sourceDocument, resultPath: path, value }); }); } const DRAFTS_FOLDER = "drafts", VERSION_FOLDER = "versions", PATH_SEPARATOR = ".", DRAFTS_PREFIX = `${DRAFTS_FOLDER}${PATH_SEPARATOR}`, VERSION_PREFIX = `${VERSION_FOLDER}${PATH_SEPARATOR}`; function isDraftId(id) { return id.startsWith(DRAFTS_PREFIX); } function isVersionId(id) { return id.startsWith(VERSION_PREFIX); } function isPublishedId(id) { return !isDraftId(id) && !isVersionId(id); } function getVersionFromId(id) { if (!isVersionId(id)) return; const [_versionPrefix, versionId, ..._publishedId] = id.split(PATH_SEPARATOR); return versionId; } function getPublishedId(id) { return isVersionId(id) ? id.split(PATH_SEPARATOR).slice(2).join(PATH_SEPARATOR) : isDraftId(id) ? id.slice(DRAFTS_PREFIX.length) : id; } function createEditUrl(options) { const { baseUrl, workspace: _workspace = "default", tool: _tool = "default", id: _id, type, path, projectId, dataset } = options; if (!baseUrl) throw new Error("baseUrl is required"); if (!path) throw new Error("path is required"); if (!_id) throw new Error("id is required"); if (baseUrl !== "/" && baseUrl.endsWith("/")) throw new Error("baseUrl must not end with a slash"); const workspace = _workspace === "default" ? void 0 : _workspace, tool = _tool === "default" ? void 0 : _tool, id = getPublishedId(_id), stringifiedPath = Array.isArray(path) ? toString(jsonPathToStudioPath(path)) : path, searchParams = new URLSearchParams({ baseUrl, id, type, path: stringifiedPath }); if (workspace && searchParams.set("workspace", workspace), tool && searchParams.set("tool", tool), projectId && searchParams.set("projectId", projectId), dataset && searchParams.set("dataset", dataset), isPublishedId(_id)) searchParams.set("perspective", "published"); else if (isVersionId(_id)) { const versionId = getVersionFromId(_id); searchParams.set("perspective", versionId); } const segments = [baseUrl === "/" ? "" : baseUrl]; workspace && segments.push(workspace); const routerParams = [ "mode=presentation", `id=${id}`, `type=${type}`, `path=${encodeURIComponent(stringifiedPath)}` ]; return tool && routerParams.push(`tool=${tool}`), segments.push("intent", "edit", `${routerParams.join(";")}?${searchParams}`), segments.join("/"); } function resolveStudioBaseRoute(studioUrl) { let baseUrl = typeof studioUrl == "string" ? studioUrl : studioUrl.baseUrl; return baseUrl !== "/" && (baseUrl = baseUrl.replace(/\/$/, "")), typeof studioUrl == "string" ? { baseUrl } : { ...studioUrl, baseUrl }; } const filterDefault = ({ sourcePath, resultPath, value }) => { if (isValidDate(value) || isValidURL(value)) return !1; const endPath = sourcePath.at(-1); return !(sourcePath.at(-2) === "slug" && endPath === "current" || typeof endPath == "string" && (endPath.startsWith("_") || endPath.endsWith("Id")) || sourcePath.some( (path) => path === "meta" || path === "metadata" || path === "openGraph" || path === "seo" ) || hasTypeLike(sourcePath) || hasTypeLike(resultPath) || typeof endPath == "string" && denylist.has(endPath)); }, denylist = /* @__PURE__ */ new Set([ "color", "colour", "currency", "email", "format", "gid", "hex", "href", "hsl", "hsla", "icon", "id", "index", "key", "language", "layout", "link", "linkAction", "locale", "lqip", "page", "path", "ref", "rgb", "rgba", "route", "secret", "slug", "status", "tag", "template", "theme", "type", "textTheme", "unit", "url", "username", "variant", "website" ]); function isValidDate(dateString) { return /^\d{4}-\d{2}-\d{2}/.test(dateString) ? !!Date.parse(dateString) : !1; } const allowedProtocols = /* @__PURE__ */ new Set([ "app:", "data:", "discord:", "file:", "ftp:", "ftps:", "geo:", "http:", "https:", "imap:", "javascript:", "magnet:", "mailto:", "maps:", "ms-excel:", "ms-powerpoint:", "ms-word:", "slack:", "sms:", "spotify:", "steam:", "teams:", "tel:", "vscode:", "zoom:" ]); function isValidURL(url) { try { const { protocol } = new URL(url, url.startsWith("/") ? "https://acme.com" : void 0); return allowedProtocols.has(protocol) || protocol.startsWith("web+"); } catch { return !1; } } function hasTypeLike(path) { return path.some((segment) => typeof segment == "string" && segment.match(/type/i) !== null); } const ZERO_WIDTHS = [ 8203, // U+200B ZERO WIDTH SPACE 8204, // U+200C ZERO WIDTH NON-JOINER 8205, // U+200D ZERO WIDTH JOINER 65279 // U+FEFF ZERO WIDTH NO-BREAK SPACE ], ZERO_WIDTHS_CHAR_CODES = ZERO_WIDTHS.map((x) => String.fromCharCode(x)), LEGACY_WIDTHS = [ 8203, 8204, 8205, 8290, 8291, 8288, 65279, 8289, 119155, 119156, 119157, 119158, 119159, 119160, 119161, 119162 ]; Object.fromEntries(ZERO_WIDTHS.map((cp, i) => [cp, i])); Object.fromEntries(LEGACY_WIDTHS.map((cp, i) => [cp, i.toString(16)])); const PREFIX = String.fromCodePoint(ZERO_WIDTHS[0]).repeat(4), ALL_WIDTHS = [...ZERO_WIDTHS, ...LEGACY_WIDTHS]; ALL_WIDTHS.map((cp) => `\\u{${cp.toString(16)}}`).join(""); function stegaEncode(data) { if (data === void 0) return ""; const json = typeof data == "string" ? data : JSON.stringify(data), bytes = new TextEncoder().encode(json); let out = ""; for (let i = 0; i < bytes.length; i++) { const b = bytes[i]; out += ZERO_WIDTHS_CHAR_CODES[b >> 6 & 3] + ZERO_WIDTHS_CHAR_CODES[b >> 4 & 3] + ZERO_WIDTHS_CHAR_CODES[b >> 2 & 3] + ZERO_WIDTHS_CHAR_CODES[b & 3]; } return PREFIX + out; } function stegaCombine(visible, metadata, skip = "auto") { return skip === !0 || skip === "auto" && !isDateLike(visible) && !isUrlLike(visible) ? `${visible}${stegaEncode(metadata)}` : visible; } function isUrlLike(t) { try { return new URL(t, t.startsWith("/") ? "https://example.com" : void 0), !0; } catch { return !1; } } function isDateLike(t) { return !t || typeof t != "string" ? !1 : !!Date.parse(t); } const TRUNCATE_LENGTH = 20; function stegaEncodeSourceMap(result, resultSourceMap, config) { const { filter, logger, enabled } = config; if (!enabled) { const msg = "config.enabled must be true, don't call this function otherwise"; throw logger?.error?.(`[@sanity/client]: ${msg}`, { result, resultSourceMap, config }), new TypeError(msg); } if (!resultSourceMap) return logger?.error?.("[@sanity/client]: Missing Content Source Map from response body", { result, resultSourceMap, config }), result; if (!config.studioUrl) { const msg = "config.studioUrl must be defined"; throw logger?.error?.(`[@sanity/client]: ${msg}`, { result, resultSourceMap, config }), new TypeError(msg); } const report = { encoded: [], skipped: [] }, resultWithStega = encodeIntoResult( result, resultSourceMap, ({ sourcePath, sourceDocument, resultPath, value }) => { if ((typeof filter == "function" ? filter({ sourcePath, resultPath, filterDefault, sourceDocument, value }) : filterDefault({ sourcePath, resultPath, value })) === !1) return logger && report.skipped.push({ path: prettyPathForLogging(sourcePath), value: `${value.slice(0, TRUNCATE_LENGTH)}${value.length > TRUNCATE_LENGTH ? "..." : ""}`, length: value.length }), value; logger && report.encoded.push({ path: prettyPathForLogging(sourcePath), value: `${value.slice(0, TRUNCATE_LENGTH)}${value.length > TRUNCATE_LENGTH ? "..." : ""}`, length: value.length }); const { baseUrl, workspace, tool } = resolveStudioBaseRoute( typeof config.studioUrl == "function" ? config.studioUrl(sourceDocument) : config.studioUrl ); if (!baseUrl) return value; const { _id: id, _type: type, _projectId: projectId, _dataset: dataset } = sourceDocument; return stegaCombine( value, { origin: "sanity.io", href: createEditUrl({ baseUrl, workspace, tool, id, type, path: sourcePath, ...!config.omitCrossDatasetReferenceData && { dataset, projectId } }) }, // We use custom logic to determine if we should skip encoding !0 ); } ); if (logger) { const isSkipping = report.skipped.length, isEncoding = report.encoded.length; if ((isSkipping || isEncoding) && ((logger?.groupCollapsed || logger.log)?.("[@sanity/client]: Encoding source map into result"), logger.log?.( `[@sanity/client]: Paths encoded: ${report.encoded.length}, skipped: ${report.skipped.length}` )), report.encoded.length > 0 && (logger?.log?.("[@sanity/client]: Table of encoded paths"), (logger?.table || logger.log)?.(report.encoded)), report.skipped.length > 0) { const skipped = /* @__PURE__ */ new Set(); for (const { path } of report.skipped) skipped.add(path.replace(reKeySegment, "0").replace(/\[\d+\]/g, "[]")); logger?.log?.("[@sanity/client]: List of skipped paths", [...skipped.values()]); } (isSkipping || isEncoding) && logger?.groupEnd?.(); } return resultWithStega; } function prettyPathForLogging(path) { return toString(jsonPathToStudioPath(path)); } var stegaEncodeSourceMap$1 = /* @__PURE__ */ Object.freeze({ __proto__: null, stegaEncodeSourceMap }); export { encodeIntoResult, stegaEncodeSourceMap, stegaEncodeSourceMap$1 }; //# sourceMappingURL=stegaEncodeSourceMap.js.map