@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
443 lines (442 loc) • 15.2 kB
JavaScript
;
var stegaClean = require("./stegaClean.cjs");
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 (stegaClean.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 (stegaClean.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
});
exports.encodeIntoResult = encodeIntoResult;
exports.stegaEncodeSourceMap = stegaEncodeSourceMap;
exports.stegaEncodeSourceMap$1 = stegaEncodeSourceMap$1;
//# sourceMappingURL=stegaEncodeSourceMap.cjs.map