UNPKG

storyblok-js-client

Version:
622 lines (621 loc) 20.5 kB
/** * name: storyblok-js-client * (c) 2025 * description: Universal JavaScript SDK for Storyblok's API * author: Alexander Feiglstorfer <delooks@gmail.com> */ var b = Object.defineProperty; var A = (c, e, t) => e in c ? b(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t; var u = (c, e, t) => A(c, typeof e != "symbol" ? e + "" : e, t); class w extends Error { constructor(e) { super(e), this.name = "AbortError"; } } function T(c, e, t) { if (!Number.isFinite(e)) throw new TypeError("Expected `limit` to be a finite number"); if (!Number.isFinite(t)) throw new TypeError("Expected `interval` to be a finite number"); const i = []; let s = [], r = 0, n = !1; const o = async () => { r++; const a = i.shift(); if (a) try { const d = await c(...a.args); a.resolve(d); } catch (d) { a.reject(d); } const h = setTimeout(() => { r--, i.length > 0 && o(), s = s.filter((d) => d !== h); }, t); s.includes(h) || s.push(h); }, l = (...a) => n ? Promise.reject( new Error( "Throttled function is already aborted and not accepting new promises" ) ) : new Promise((h, d) => { i.push({ resolve: h, reject: d, args: a }), r < e && o(); }); return l.abort = () => { n = !0, s.forEach(clearTimeout), s = [], i.forEach( (a) => a.reject(() => new w("Throttle function aborted")) ), i.length = 0; }, l; } const S = (c = "") => c.includes("/cdn/"), P = (c, e = 25, t = 1) => ({ ...c, per_page: e, page: t }), C = (c) => new Promise((e) => setTimeout(e, c)), $ = (c = 0, e) => Array.from({ length: c }, e), E = (c = 0, e = c) => { const t = Math.abs(e - c) || 0, i = c < e ? 1 : -1; return $(t, (s, r) => r * i + c); }, L = async (c, e) => Promise.all(c.map(e)), O = (c = [], e) => c.map(e).reduce((t, i) => [...t, ...i], []), k = (c, e, t) => { const i = []; for (const s in c) { if (!Object.prototype.hasOwnProperty.call(c, s)) continue; const r = c[s]; if (r == null) continue; const n = t ? "" : encodeURIComponent(s); let o; typeof r == "object" ? o = k( r, e ? e + encodeURIComponent(`[${n}]`) : n, Array.isArray(r) ) : o = `${e ? e + encodeURIComponent(`[${n}]`) : n}=${encodeURIComponent(r)}`, i.push(o); } return i.join("&"); }, v = (c) => { const e = { eu: "api.storyblok.com", us: "api-us.storyblok.com", cn: "app.storyblokchina.cn", ap: "api-ap.storyblok.com", ca: "api-ca.storyblok.com" }; return e[c] ?? e.eu; }, H = (c) => { const e = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }, t = /[&<>"']/g, i = new RegExp(t.source); return c && i.test(c) ? c.replace(t, (s) => e[s]) : c; }; class N { constructor(e) { u(this, "baseURL"); u(this, "timeout"); u(this, "headers"); u(this, "responseInterceptor"); u(this, "fetch"); u(this, "ejectInterceptor"); u(this, "url"); u(this, "parameters"); u(this, "fetchOptions"); this.baseURL = e.baseURL, this.headers = e.headers || new Headers(), this.timeout = e != null && e.timeout ? e.timeout * 1e3 : 0, this.responseInterceptor = e.responseInterceptor, this.fetch = (...t) => e.fetch ? e.fetch(...t) : fetch(...t), this.ejectInterceptor = !1, this.url = "", this.parameters = {}, this.fetchOptions = {}; } /** * * @param url string * @param params ISbStoriesParams * @returns Promise<ISbResponse | Error> */ get(e, t) { return this.url = e, this.parameters = t, this._methodHandler("get"); } post(e, t) { return this.url = e, this.parameters = t, this._methodHandler("post"); } put(e, t) { return this.url = e, this.parameters = t, this._methodHandler("put"); } delete(e, t) { return this.url = e, this.parameters = t ?? {}, this._methodHandler("delete"); } async _responseHandler(e) { const t = [], i = { data: {}, headers: {}, status: 0, statusText: "" }; e.status !== 204 && await e.json().then((s) => { i.data = s; }); for (const s of e.headers.entries()) t[s[0]] = s[1]; return i.headers = { ...t }, i.status = e.status, i.statusText = e.statusText, i; } async _methodHandler(e) { let t = `${this.baseURL}${this.url}`, i = null; e === "get" ? t = `${this.baseURL}${this.url}?${k(this.parameters)}` : i = JSON.stringify(this.parameters); const s = new URL(t), r = new AbortController(), { signal: n } = r; let o; this.timeout && (o = setTimeout(() => r.abort(), this.timeout)); try { const l = await this.fetch(`${s}`, { method: e, headers: this.headers, body: i, signal: n, ...this.fetchOptions }); this.timeout && clearTimeout(o); const a = await this._responseHandler( l ); return this.responseInterceptor && !this.ejectInterceptor ? this._statusHandler(this.responseInterceptor(a)) : this._statusHandler(a); } catch (l) { return { message: l }; } } setFetchOptions(e = {}) { Object.keys(e).length > 0 && "method" in e && delete e.method, this.fetchOptions = { ...e }; } eject() { this.ejectInterceptor = !0; } /** * Normalizes error messages from different response structures * @param data The response data that might contain error information * @returns A normalized error message string */ _normalizeErrorMessage(e) { if (Array.isArray(e)) return e[0] || "Unknown error"; if (e && typeof e == "object") { if (e.error) return e.error; for (const t in e) { if (Array.isArray(e[t])) return `${t}: ${e[t][0]}`; if (typeof e[t] == "string") return `${t}: ${e[t]}`; } if (e.slug) return e.slug; } return "Unknown error"; } _statusHandler(e) { const t = /20[0-6]/g; return new Promise((i, s) => { if (t.test(`${e.status}`)) return i(e); const r = { message: this._normalizeErrorMessage(e.data), status: e.status, response: e }; s(r); }); } } const _ = "SB-Agent", g = { defaultAgentName: "SB-JS-CLIENT", defaultAgentVersion: "SB-Agent-Version", packageVersion: "6.0.0" }, R = { DRAFT: "draft", PUBLISHED: "published" }, x = Object.values( R ); let p = {}; const f = {}; class V { /** * * @param config ISbConfig interface * @param pEndpoint string, optional */ constructor(e, t) { u(this, "client"); u(this, "maxRetries"); u(this, "retriesDelay"); u(this, "throttle"); u(this, "accessToken"); u(this, "cache"); u(this, "resolveCounter"); u(this, "relations"); u(this, "links"); u(this, "version"); /** * @deprecated This property is deprecated. Use the standalone `richTextResolver` from `@storyblok/richtext` instead. * @see https://github.com/storyblok/richtext */ u(this, "richTextResolver"); u(this, "resolveNestedRelations"); u(this, "stringifiedStoriesCache"); u(this, "inlineAssets"); let i = e.endpoint || t; if (!i) { const n = e.https === !1 ? "http" : "https"; e.oauthToken ? i = `${n}://${v(e.region)}/v1` : i = `${n}://${v(e.region)}/v2`; } const s = new Headers(); s.set("Content-Type", "application/json"), s.set("Accept", "application/json"), e.headers && (e.headers.constructor.name === "Headers" ? e.headers.entries().toArray() : Object.entries(e.headers)).forEach(([o, l]) => { s.set(o, l); }), s.has(_) || (s.set(_, g.defaultAgentName), s.set( g.defaultAgentVersion, g.packageVersion )); let r = 5; e.oauthToken && (s.set("Authorization", e.oauthToken), r = 3), e.rateLimit && (r = e.rateLimit), this.maxRetries = e.maxRetries || 10, this.retriesDelay = 300, this.throttle = T( this.throttledRequest.bind(this), r, 1e3 ), this.accessToken = e.accessToken || "", this.relations = {}, this.links = {}, this.cache = e.cache || { clear: "manual" }, this.resolveCounter = 0, this.resolveNestedRelations = e.resolveNestedRelations || !0, this.stringifiedStoriesCache = {}, this.version = e.version || R.DRAFT, this.inlineAssets = e.inlineAssets || !1, this.client = new N({ baseURL: i, timeout: e.timeout || 0, headers: s, responseInterceptor: e.responseInterceptor, fetch: e.fetch }); } parseParams(e) { return e.token || (e.token = this.getToken()), e.cv || (e.cv = f[e.token]), Array.isArray(e.resolve_relations) && (e.resolve_relations = e.resolve_relations.join(",")), typeof e.resolve_relations < "u" && (e.resolve_level = 2), e; } factoryParamOptions(e, t) { return S(e) ? this.parseParams(t) : t; } makeRequest(e, t, i, s, r) { const n = this.factoryParamOptions( e, P(t, i, s) ); return this.cacheResponse(e, n, void 0, r); } get(e, t = {}, i) { t || (t = {}); const s = `/${e}`; t.version = t.version || this.version; const r = this.factoryParamOptions(s, t); return this.cacheResponse(s, r, void 0, i); } async getAll(e, t = {}, i, s) { const r = (t == null ? void 0 : t.per_page) || 25, n = `/${e}`.replace(/\/$/, ""), o = i ?? n.substring(n.lastIndexOf("/") + 1); t.version = t.version || this.version; const l = 1, a = await this.makeRequest( n, t, r, l, s ), h = a.total ? Math.ceil(a.total / r) : 1, d = await L( E(l, h), (y) => this.makeRequest(n, t, r, y + 1, s) ); return O([a, ...d], (y) => Object.values(y.data[o])); } post(e, t = {}, i) { const s = `/${e}`; return this.throttle("post", s, t, i); } put(e, t = {}, i) { const s = `/${e}`; return this.throttle("put", s, t, i); } delete(e, t = {}, i) { t || (t = {}); const s = `/${e}`; return this.throttle("delete", s, t, i); } getStories(e = {}, t) { return this._addResolveLevel(e), this.get("cdn/stories", e, t); } getStory(e, t = {}, i) { return this._addResolveLevel(t), this.get(`cdn/stories/${e}`, t, i); } getToken() { return this.accessToken; } ejectInterceptor() { this.client.eject(); } _addResolveLevel(e) { typeof e.resolve_relations < "u" && (e.resolve_level = 2); } _cleanCopy(e) { return JSON.parse(JSON.stringify(e)); } _insertLinks(e, t, i) { const s = e[t]; s && s.fieldtype === "multilink" && s.linktype === "story" && typeof s.id == "string" && this.links[i][s.id] ? s.story = this._cleanCopy(this.links[i][s.id]) : s && s.linktype === "story" && typeof s.uuid == "string" && this.links[i][s.uuid] && (s.story = this._cleanCopy(this.links[i][s.uuid])); } /** * * @param resolveId A counter number as a string * @param uuid The uuid of the story * @returns string | object */ getStoryReference(e, t) { return this.relations[e][t] ? JSON.parse(this.stringifiedStoriesCache[t] || JSON.stringify(this.relations[e][t])) : t; } /** * Resolves a field's value by replacing UUIDs with their corresponding story references * @param jtree - The JSON tree object containing the field to resolve * @param treeItem - The key of the field to resolve * @param resolveId - The unique identifier for the current resolution context * * This method handles both single string UUIDs and arrays of UUIDs: * - For single strings: directly replaces the UUID with the story reference * - For arrays: maps through each UUID and replaces with corresponding story references */ _resolveField(e, t, i) { const s = e[t]; typeof s == "string" ? e[t] = this.getStoryReference(i, s) : Array.isArray(s) && (e[t] = s.map( (r) => this.getStoryReference(i, r) ).filter(Boolean)); } /** * Inserts relations into the JSON tree by resolving references * @param jtree - The JSON tree object to process * @param treeItem - The current field being processed * @param fields - The relation patterns to resolve (string or array of strings) * @param resolveId - The unique identifier for the current resolution context * * This method handles two types of relation patterns: * 1. Nested relations: matches fields that end with the current field name * Example: If treeItem is "event_type", it matches patterns like "*.event_type" * * 2. Direct component relations: matches exact component.field patterns * Example: "event.event_type" for component "event" and field "event_type" * * The method supports both string and array formats for the fields parameter, * allowing flexible specification of relation patterns. */ _insertRelations(e, t, i, s) { if (Array.isArray(i) ? i.find((o) => o.endsWith(`.${t}`)) : i.endsWith(`.${t}`)) { this._resolveField(e, t, s); return; } const n = e.component ? `${e.component}.${t}` : t; (Array.isArray(i) ? i.includes(n) : i === n) && this._resolveField(e, t, s); } /** * Recursively traverses and resolves relations in the story content tree * @param story - The story object containing the content to process * @param fields - The relation patterns to resolve * @param resolveId - The unique identifier for the current resolution context */ iterateTree(e, t, i) { const s = (r, n = "") => { if (!(!r || r._stopResolving)) { if (Array.isArray(r)) r.forEach((o, l) => s(o, `${n}[${l}]`)); else if (typeof r == "object") for (const o in r) { const l = n ? `${n}.${o}` : o; (r.component && r._uid || r.type === "link") && (this._insertRelations(r, o, t, i), this._insertLinks(r, o, i)), s(r[o], l); } } }; s(e.content); } async resolveLinks(e, t, i) { let s = []; if (e.link_uuids) { const r = e.link_uuids.length, n = [], o = 50; for (let l = 0; l < r; l += o) { const a = Math.min(r, l + o); n.push(e.link_uuids.slice(l, a)); } for (let l = 0; l < n.length; l++) (await this.getStories({ per_page: o, language: t.language, version: t.version, starts_with: t.starts_with, by_uuids: n[l].join(",") })).data.stories.forEach( (h) => { s.push(h); } ); } else s = e.links; s.forEach((r) => { this.links[i][r.uuid] = { ...r, _stopResolving: !0 }; }); } async resolveRelations(e, t, i) { let s = []; if (e.rel_uuids) { const r = e.rel_uuids.length, n = [], o = 50; for (let l = 0; l < r; l += o) { const a = Math.min(r, l + o); n.push(e.rel_uuids.slice(l, a)); } for (let l = 0; l < n.length; l++) (await this.getStories({ per_page: o, language: t.language, version: t.version, starts_with: t.starts_with, by_uuids: n[l].join(","), excluding_fields: t.excluding_fields })).data.stories.forEach((h) => { s.push(h); }); s.length > 0 && (e.rels = s, delete e.rel_uuids); } else s = e.rels; s && s.length > 0 && s.forEach((r) => { this.relations[i][r.uuid] = { ...r, _stopResolving: !0 }; }); } /** * * @param responseData * @param params * @param resolveId * @description Resolves the relations and links of the stories * @returns Promise<void> * */ async resolveStories(e, t, i) { var r, n; let s = []; if (this.links[i] = {}, this.relations[i] = {}, typeof t.resolve_relations < "u" && t.resolve_relations.length > 0 && (typeof t.resolve_relations == "string" && (s = t.resolve_relations.split(",")), await this.resolveRelations(e, t, i)), t.resolve_links && ["1", "story", "url", "link"].includes(t.resolve_links) && ((r = e.links) != null && r.length || (n = e.link_uuids) != null && n.length) && await this.resolveLinks(e, t, i), this.resolveNestedRelations) for (const o in this.relations[i]) this.iterateTree( this.relations[i][o], s, i ); e.story ? this.iterateTree(e.story, s, i) : e.stories.forEach((o) => { this.iterateTree(o, s, i); }), this.stringifiedStoriesCache = {}, delete this.links[i], delete this.relations[i]; } async cacheResponse(e, t, i, s) { const r = k({ url: e, params: t }), n = this.cacheProvider(); if (t.version === "published" && e !== "/cdn/spaces/me") { const o = await n.get(r); if (o) return Promise.resolve(o); } return new Promise(async (o, l) => { var a; try { const h = await this.throttle( "get", e, t, s ); if (h.status !== 200) return l(h); let d = { data: h.data, headers: h.headers }; if ((a = h.headers) != null && a["per-page"] && (d = Object.assign({}, d, { perPage: h.headers["per-page"] ? Number.parseInt(h.headers["per-page"]) : 0, total: h.headers["per-page"] ? Number.parseInt(h.headers.total) : 0 })), d.data.story || d.data.stories) { const m = this.resolveCounter = ++this.resolveCounter % 1e3; await this.resolveStories(d.data, t, `${m}`), d = await this.processInlineAssets(d); } t.version === "published" && e !== "/cdn/spaces/me" && await n.set(r, d); const y = this.cache.clear === "onpreview" && t.version === "draft" || this.cache.clear === "auto"; return t.token && d.data.cv && (y && f[t.token] && f[t.token] !== d.data.cv && await this.flushCache(), f[t.token] = d.data.cv), o(d); } catch (h) { if (h.response && h.status === 429 && (i = typeof i > "u" ? 0 : i + 1, i < this.maxRetries)) return console.log( `Hit rate limit. Retrying in ${this.retriesDelay / 1e3} seconds.` ), await C(this.retriesDelay), this.cacheResponse(e, t, i).then(o).catch(l); l(h); } }); } throttledRequest(e, t, i, s) { return this.client.setFetchOptions(s), this.client[e](t, i); } cacheVersions() { return f; } cacheVersion() { return f[this.accessToken]; } setCacheVersion(e) { this.accessToken && (f[this.accessToken] = e); } clearCacheVersion() { this.accessToken && (f[this.accessToken] = 0); } cacheProvider() { switch (this.cache.type) { case "memory": return { get(e) { return Promise.resolve(p[e]); }, getAll() { return Promise.resolve(p); }, set(e, t) { return p[e] = t, Promise.resolve(void 0); }, flush() { return p = {}, Promise.resolve(void 0); } }; case "custom": if (this.cache.custom) return this.cache.custom; default: return { get() { return Promise.resolve(); }, getAll() { return Promise.resolve(void 0); }, set() { return Promise.resolve(void 0); }, flush() { return Promise.resolve(void 0); } }; } } async flushCache() { return await this.cacheProvider().flush(), this.clearCacheVersion(), this; } async processInlineAssets(e) { if (!this.inlineAssets) return e; const t = (i) => { if (!i || typeof i != "object") return i; if (Array.isArray(i)) return i.map((r) => t(r)); let s = { ...i }; s.fieldtype === "asset" && Array.isArray(e.data.assets) && (s = { ...s, ...e.data.assets.find((r) => r.id === s.id) }); for (const r in s) typeof s[r] == "object" && (s[r] = t(s[r])); return s; }; return e.data.story && (e.data.story.content = t(e.data.story.content)), e.data.stories && (e.data.stories = e.data.stories.map((i) => (i.content = t(i.content), i))), e; } } export { _ as STORYBLOK_AGENT, g as STORYBLOK_JS_CLIENT_AGENT, N as SbFetch, R as StoryblokContentVersion, x as StoryblokContentVersionValues, $ as arrayFrom, L as asyncMap, V as default, C as delay, H as escapeHTML, O as flatMap, P as getOptionsPage, v as getRegionURL, S as isCDNUrl, E as range, k as stringify };