@apptus/esales-api
Version:
Library for making requests to Elevate 4 API v3
599 lines (587 loc) • 20.7 kB
JavaScript
;
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