UNPKG

@apptus/esales-api

Version:

Library for making requests to Elevate 4 API v3

599 lines (587 loc) 20.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/mod.ts var mod_exports = {}; __export(mod_exports, { elevate: () => elevate, esales: () => esales, isCheckboxFacet: () => isCheckboxFacet, isColorFacet: () => isColorFacet, isRangeFacet: () => isRangeFacet, isSizeFacet: () => isSizeFacet, isTextFacet: () => isTextFacet, localStorageBackedSession: () => localStorageBackedSession, notifications: () => notifications, queries: () => queries }); module.exports = __toCommonJS(mod_exports); // src/util/asserts.ts var AssertionError = class extends Error { constructor(message) { super(message); this.name = "AssertionError"; } }; function assert(condition, message = "") { if (!condition) throw new AssertionError(message); } function assertString(value, message) { assert(typeof value === "string" && value.length > 0, message); } // src/util/guards.ts function isDefined(value) { return value != null; } function isString(value) { return typeof value === "string" && value.length > 0; } // src/util/once.ts function once(method) { let executed = false; let returnValue; return function onceInner(...args) { if (!executed) { executed = true; returnValue = method.apply(this, args); } return returnValue; }; } // src/util/url.ts var ResponseError = class extends Error { constructor(details) { super(details.message); this.details = details; } }; async function createBaseUrl(endpoint, config) { const { market, clusterUrl, session } = config; const { customerKey, sessionKey } = await session(); const url = new URL(`/api/storefront/v3/${endpoint}`, clusterUrl); addUrlParams(url, { market, customerKey, sessionKey }); return url; } function addUrlParams(url, params) { for (const [key, value] of Object.entries(params)) { if (isDefined(value)) { const values = Array.isArray(value) ? value : [value]; url.searchParams.set(key, values.map(String).join("|")); } } } function facetsToParams(facets = {}) { const result = {}; for (const [name, value] of Object.entries(facets)) { if (Array.isArray(value) || typeof value !== "object") { result[`f.${name}`] = value; } else { if (isDefined(value.min)) result[`f.${name}.min`] = value.min; if (isDefined(value.max)) result[`f.${name}.max`] = value.max; } } return result; } // src/config.ts function validateBaseConfig(config) { assert(typeof config === "object" && config !== null, 'API config must be an "object"'); const c = config; assertString(c.clusterId, 'Property "clusterId" must be a non-empty string'); assertString(c.market, 'Property "market" must be a non-empty string'); assert(typeof c.session === "function", 'Property "session" must be a method.'); } function validateConfig(config) { validateBaseConfig(config); const c = config; const touchpoints = ["desktop", "mobile"]; assertString(c.locale, 'Property "locale" must be a string and valid locale, e.g. "en-US".'); assert(touchpoints.includes(c.touchpoint), `Property "touchpoint" must be one of: ${touchpoints.join(", ")}`); } function resolveClusterUrl(config) { const { clusterId, ...rest } = config; const clusterUrl = (() => { const normalizeUrl = (url) => new URL(url).href; try { return normalizeUrl(clusterId); } catch { } return normalizeUrl(`https://${clusterId}.api.esales.apptus.cloud/`); })(); return { ...rest, clusterUrl }; } function resolveConfig(config) { validateConfig(config); return resolveClusterUrl(config); } function resolveNotificationConfig(config) { validateBaseConfig(config); return resolveClusterUrl(config); } // src/query.ts var Query = class { constructor(__config) { this.__config = __config; } /** * Retrieve recommendations for a product that has just been added to the cart. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.addToCartPopup({ variantKey: 'variant-key-123' }, { * recommendationLists: [{ * id: 'addons', * algorithm: 'ADD_TO_CART_RECS' * }] * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/add-to-cart-popup/ */ addToCartPopup(params, recommendationLists) { return this.__query("add-to-cart-popup", { params, body: { recommendationLists } }); } /** * Autocomplete based on provided query parameter, for search suggestions, * product suggestions, and more. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.autocomplete({ q: 'shirt' }); * const sale = await api.query.autocomplete({ q: 'dress' }, { * productFilter: { discount: 50 } * p}); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/autocomplete/ */ autocomplete(params, body) { return this.__query("autocomplete", { params, body }); } /** * Retrieve the complete navigation tree, suitable for a top/mobile navigation of the site. * * @param params query parameter options to submit * * @example * ```ts * const tree = await api.query.navigationTree(); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/navigation-tree/ */ navigationTree(params) { return this.__query("navigation-tree", { params }); } /** * Returns a product listing with facets based on provided query, optionally * with related navigation included. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.searchPage({ q: 'shirt' }); * const sale = await api.query.searchPage({ q: 'dress' }, { * primaryList: { * productFilter: { discount: 50 } * }, * navigation: { include: true } * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/search-page/ */ searchPage(params, body) { const { facets, ...p } = params; return this.__query("search-page", { facets, params: { ...p }, body }); } /** * Retrieves product information and related info suitable to show on a Product page. * Can be configured to show various recommendation lists based on body configuration settings. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.productPage({ productKey: 'p123' }, { * recommendationLists: [{ * id: 'alts', * algorithm: 'ALTERNATIVES' * }] * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/product-page/ */ productPage(params, body) { return this.__query("product-page", { params, body }); } /** * Retrieve products - and possibly recommendation lists - suitable for display * on a cart page. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.cartPage({ cart: ['p123', 'p456'] }, { * recommendationLists: [{ * id: 'addons', * algorithm: 'CART' * }] * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/cart-page/ */ cartPage(params, body) { return this.__query("cart-page", { params, body }); } /** * Request for generic landing pages, or category pages. Can return product listing with facets, * recommendation lists, or both. Suitable for the start/home page, intermediate category pages, * brand pages, and more. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await api.query.landingPage(); * const sale = await api.query.landingPage({ limit: 30, skip: 0 }, { * primaryList: { * productFilter: { discount: 50 } * }, * recommendationLists: [{ * id: 'personal', * algorithm: 'PERSONAL' * }] * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/landing-page/ */ landingPage(params = {}, body) { const { facets, ...p } = params; return this.__query("landing-page", { facets, params: { ...p }, body }); } /** * Request for retrieving a list of sponsored products for a Page. * * @beta ⚠️ This request is currently limited to a private beta and will fail otherwise. * * @param params query parameter options to submit * * @example * ```ts * const res = await api.query.sponsoredPage({ * pageReference: '/women/tops' * }); * ``` * @see https://docs.apptus.com/elevate/4/integration/api/specifications/storefront/v3/queries/sponsored-page/ */ sponsoredPage(params) { return this.__query("sponsored-page", { params }); } /** * Mirrors the Product Page, but for Content instead of Products. Retrieves content * information for the provided `contentKeys` (required). * * @param params query parameter options to submit * * @example * ```ts * const res = await api.query.contentInformation({ * contentKeys: ['ck_123456', 'ck_234567'] * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/content-information/ */ contentInformation(params) { return this.__query("content-information", { params }); } /** * Mirrors the Search Page request, but for Content instead of Products. Returns content * results matching the provided query. Suiteable for searching in e.g. FAQ or customer service, * where no product results are required. * * @param params query parameter options to submit * @param body configuration options to submit * * @example * ```ts * const res = await this.contentSearchPage({ q: 'returns' }); * ``` * @example * ```ts * const res = await this.contentSearchPage({ q: 'shipping', skip: 20 }, { * primaryList: { * contentFilter: { * type: 'faq' * } * } * }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/queries/content-search-page/ */ contentSearchPage(params, body) { return this.__query("content-search-page", { params, body }); } async __query(endpoint, options) { const { params = {}, facets, body } = options; assert(typeof params === "object", "If provided, params must be an object"); const url = await createBaseUrl(`queries/${endpoint}`, this.__config); const { locale, touchpoint } = this.__config; addUrlParams(url, { locale, touchpoint, ...params, ...facetsToParams(facets) }); const init = !body ? { method: "GET" } : { method: "POST", headers: { "Content-Type": "text/plain" }, body: JSON.stringify(body) }; const res = await fetch(url, init); const content = await res.json(); if (res.status >= 200 && res.status < 400) { return content; } throw new ResponseError(content); } }; // src/notification.ts var Notification = class { constructor(__config) { this.__config = __config; } /** * Send click notification with ticket. Tries to queue fetch * request with `keepalive` option, with one retry if it fails. * * @param ticket is present on product and variant * * @example * ```ts * await api.notify.click('OzE7cHJ...yM7Mjg7'); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/click/ */ click(ticket) { return this.__notification("click", { ticket }); } /** * Send impression notification. Tries to queue fetch * request with `keepalive` option, with one retry if it fails. * * @beta ⚠️ This request is currently limited to a private beta and will fail otherwise. * * @param ticket attached to a sponsored product or list * * @example * ```ts * await api.notify.adImpression('OzE7cHJ...yM7Mjg7'); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/ad-impression/ */ adImpression(ticket) { return this.__notification("ad-impression", { ticket }); } /** * Send add to cart notification with ticket. Tries to queue fetch * request with `keepalive` option, with one retry if it fails. * * @param ticket is present on product and variant * * @example * ```ts * await api.notify.addToCart('OzE7cHJ...jOzI4Ow'); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/add-to-cart/ */ addToCart(ticket) { return this.__notification("add-to-cart", { ticket }); } /** * Send add favorite notification with product-key. Tries to queue fetch * request with `keepalive` option, with one retry if it fails. * * @param productKeyOrPayload a `Product.key` or object with variant or product key * * @example * ```ts * await api.notify.addFavorite('pk_123456'); * await api.notify.addFavorite({ variantKey: 'vk_234567' }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/add-favorite/ */ addFavorite(productKeyOrPayload) { const body = typeof productKeyOrPayload === "string" ? { productKey: productKeyOrPayload } : productKeyOrPayload; return this.__notification("add-favorite", body); } /** * Send remove favorite notification with product-key. Tries to queue fetch * request with `keepalive` option, with one retry if it fails. * * @param productKeyOrPayload a `Product.key` or object with variant or product key * * @example * ```ts * await api.notify.removeFavorite('pk_123456'); * await api.notify.removeFavorite({ variantKey: 'vk_234567' }); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/remove-favorite/ */ removeFavorite(productKeyOrPayload) { const body = typeof productKeyOrPayload === "string" ? { productKey: productKeyOrPayload } : productKeyOrPayload; return this.__notification("remove-favorite", body); } /** * Send a notification to remove recent searches for the current `customerKey`. * * The values in the array should match values on `RecentSearch.q` returned * from `Autocomplete.recentSearches`, to ensure they are removed correctly. * * Tries to queue fetch request with `keepalive` option, with one retry if it fails. * * @param phrases An array of recent search phrases to remove or the string 'removeAll' to remove all phrases. * * @example * ```ts * await api.notify.removeRecentSearches('removeAll'); * await api.notify.removeRecentSearches(['gift card']); * await api.notify.removeRecentSearches(['dress', 'jacket', 'shoes']); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/remove-recent-searches/ */ removeRecentSearches(phrases) { return this.__notification("remove-recent-searches", phrases === "removeAll" ? { removeAll: true } : { phrases }); } /** * Send a notification to remove recently viewed products for the current `customerKey`. * * When removing individual products, `Product.key` should be used as ID for specific products * to remove from Recently viewed. * * Tries to queue fetch request with `keepalive` option, with one retry if it fails. * * @param productKeys An array of productKeys to remove or the string 'removeAll' to remove all products. * * @example * ```ts * await api.notify.removeRecentlyViewed('removeAll'); * await api.notify.removeRecentlyViewed(['pk_123456']); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/remove-recently-viewed/ */ removeRecentlyViewed(productKeys) { return this.__notification("remove-recently-viewed", productKeys === "removeAll" ? { removeAll: true } : { productKeys }); } /** * Send end notification. Tries to queue fetch request with `keepalive` option, * with one retry if it fails. * * @example * ```ts * await api.notify.end(); * ``` * @see https://docs.elevate.voyado.cloud/elevate/4/integration/api/specifications/storefront/v3/notifications/end/ */ end() { return this.__notification("end"); } async __notification(endpoint, body) { const send = async (keepalive) => { const url = await createBaseUrl(`notifications/${endpoint}`, this.__config); const data = body ? JSON.stringify(body) : void 0; const res = await fetch(url, { method: "POST", body: data, keepalive }); assert(res.ok); }; try { await send(true); } catch { await send(false); } } }; // src/helpers/type.ts var isTextFacet = (facet) => facet.type === "TEXT"; var isColorFacet = (facet) => facet.type === "COLOR"; var isRangeFacet = (facet) => facet.type === "RANGE"; var isSizeFacet = (facet) => facet.type === "SIZE"; var isCheckboxFacet = (facet) => facet.type === "CHECKBOX"; // src/session.ts var __storage = /* @__PURE__ */ (() => globalThis.localStorage)(); var __sessionMetadataCache = /* @__PURE__ */ new Map(); function localStorageBackedSession(storageKey = "voyado.session") { enableCachePruning(); const fetcher = () => read(storageKey); fetcher.updateCustomerKey = (customerKey) => update(storageKey, { ...fetcher(), customerKey }); fetcher.reset = () => update(storageKey, generateSession()); return fetcher; } var enableCachePruning = /* @__PURE__ */ once(() => { globalThis.addEventListener("storage", ({ key, storageArea }) => { if (key && storageArea === __storage) __sessionMetadataCache.delete(key); }); }); function generateSession() { return { customerKey: crypto.randomUUID(), sessionKey: crypto.randomUUID() }; } function read(key) { if (!__sessionMetadataCache.has(key)) { const strData = __storage.getItem(key); const session = generateSession(); try { assertString(strData); const d = JSON.parse(strData); assert(d && typeof d === "object"); if ("customerKey" in d && isString(d.customerKey)) session.customerKey = d.customerKey; if ("sessionKey" in d && isString(d.sessionKey)) session.sessionKey = d.sessionKey; update(key, session, strData); } catch { update(key, session); } } return __sessionMetadataCache.get(key); } function update(key, data, prevData = "") { updateCache(key, data); const currData = JSON.stringify(data); if (prevData !== currData) __storage.setItem(key, currData); } function updateCache(key, data) { __sessionMetadataCache.set(key, data); } // src/mod.ts function elevate(config) { const resolvedConfig = resolveConfig(config); return Object.freeze({ /** The base URL to the Elevate cluster hosting the API */ clusterUrl: resolvedConfig.clusterUrl, /** Send queries to retrieve products, facets and more */ query: new Query(resolvedConfig), /** Send notifications for clicks, add-to-carts and more */ notify: new Notification(resolvedConfig) }); } var esales = elevate; function queries(config) { return new Query(resolveConfig(config)); } function notifications(config) { return new Notification(resolveNotificationConfig(config)); } //# sourceMappingURL=mod.cjs.map