@techsio/storefront-data
Version:
Medusa-first storefront data layer built on TanStack Query.
156 lines (155 loc) • 6.55 kB
JavaScript
import { resolvePositiveInteger } from "./utils.js";
const EMPTY_FACETS = {
status: [],
form: [],
brand: [],
ingredient: [],
price: {
min: null,
max: null
}
};
const resolveNonNegativeInteger = (value, fallbackValue)=>{
if ("number" != typeof value || Number.isNaN(value) || !Number.isFinite(value)) return fallbackValue;
const normalizedValue = Math.trunc(value);
if (normalizedValue < 0) return fallbackValue;
return normalizedValue;
};
const normalizeNonNegativeNumber = (value)=>{
if ("number" != typeof value || Number.isNaN(value) || !Number.isFinite(value)) return;
if (value < 0) return;
return value;
};
const normalizeStringArray = (values)=>{
if (!Array.isArray(values)) return;
const seenValues = new Set();
const normalizedValues = [];
for (const rawValue of values){
if ("string" != typeof rawValue) continue;
const value = rawValue.trim();
if (!(!value || seenValues.has(value))) {
seenValues.add(value);
normalizedValues.push(value);
}
}
return normalizedValues.length > 0 ? normalizedValues : void 0;
};
const normalizeFacetItems = (items)=>{
if (!Array.isArray(items)) return [];
return items.map((item)=>{
if (!item || "object" != typeof item || Array.isArray(item)) return null;
const typedItem = item;
const id = "string" == typeof typedItem.id ? typedItem.id.trim() : "";
const label = "string" == typeof typedItem.label ? typedItem.label.trim() : "";
const count = "number" == typeof typedItem.count && Number.isFinite(typedItem.count) ? typedItem.count : 0;
if (!(id && label)) return null;
return {
id,
label,
count
};
}).filter((item)=>Boolean(item));
};
const normalizeFacets = (value)=>{
if (!value || "object" != typeof value || Array.isArray(value)) return EMPTY_FACETS;
const facetsRecord = value;
const priceRecord = facetsRecord.price && "object" == typeof facetsRecord.price && !Array.isArray(facetsRecord.price) ? facetsRecord.price : {};
const min = "number" == typeof priceRecord.min && Number.isFinite(priceRecord.min) ? priceRecord.min : null;
const max = "number" == typeof priceRecord.max && Number.isFinite(priceRecord.max) ? priceRecord.max : null;
return {
status: normalizeFacetItems(facetsRecord.status),
form: normalizeFacetItems(facetsRecord.form),
brand: normalizeFacetItems(facetsRecord.brand),
ingredient: normalizeFacetItems(facetsRecord.ingredient),
price: {
min,
max
}
};
};
const toCsv = (values)=>{
if (!values || 0 === values.length) return;
return values.join(",");
};
const stripNullishValues = (input)=>{
const result = {};
for (const [key, value] of Object.entries(input))if (null != value) result[key] = value;
return result;
};
const buildDefaultListQuery = (params, defaults)=>{
const normalizedPage = resolvePositiveInteger(params.page, 1);
const normalizedLimit = resolvePositiveInteger(params.limit, defaults.defaultLimit);
const normalizedPriceMin = normalizeNonNegativeNumber(params.price_min);
const normalizedPriceMax = normalizeNonNegativeNumber(params.price_max);
const normalizedStatus = normalizeStringArray(params.status);
const normalizedForm = normalizeStringArray(params.form);
const normalizedBrand = normalizeStringArray(params.brand);
const normalizedIngredient = normalizeStringArray(params.ingredient);
const normalizedCategoryIds = normalizeStringArray(params.category_id);
const normalizedSaleSelection = true === params.on_sale ? true : toCsv(normalizeStringArray(params.on_sale));
return stripNullishValues({
q: params.q?.trim() || void 0,
page: normalizedPage,
limit: normalizedLimit,
sort: params.sort || defaults.defaultSort,
category_id: toCsv(normalizedCategoryIds),
status: toCsv(normalizedStatus),
form: toCsv(normalizedForm),
brand: toCsv(normalizedBrand),
ingredient: toCsv(normalizedIngredient),
price_min: normalizedPriceMin,
price_max: normalizedPriceMax,
region_id: params.region_id,
country_code: params.country_code?.toLowerCase(),
currency_code: params.currency_code?.toLowerCase(),
locale: params.locale,
on_sale: normalizedSaleSelection
});
};
function createMedusaCatalogService(sdk, config) {
const { listPath = "/store/catalog/products", defaultLimit = 12, defaultSort = "recommended", queryDefaults, normalizeListQuery, transformProduct, transformListProduct, transformFacets } = config ?? {};
const baseTransform = transformProduct ?? ((product)=>product);
const mapFacets = transformFacets ?? ((facets)=>facets);
const mapListProduct = transformListProduct ?? ((product)=>baseTransform(product));
const buildListQuery = (params)=>{
if (normalizeListQuery) return stripNullishValues({
...queryDefaults,
...normalizeListQuery(params)
});
return stripNullishValues({
...queryDefaults,
...buildDefaultListQuery(params, {
defaultLimit,
defaultSort
})
});
};
return {
async getCatalogProducts (params, signal) {
const query = buildListQuery(params);
const rawResponse = await sdk.client.fetch(listPath, {
query,
signal
});
const normalizedResponse = {
products: rawResponse.products ?? [],
count: rawResponse.count ?? 0,
page: resolvePositiveInteger(rawResponse.page, resolvePositiveInteger(params.page, 1)),
limit: resolvePositiveInteger(rawResponse.limit, resolvePositiveInteger(params.limit, defaultLimit)),
totalPages: resolveNonNegativeInteger(rawResponse.totalPages, 0),
facets: mapFacets(normalizeFacets(rawResponse.facets))
};
const context = {
params,
query,
response: normalizedResponse
};
const products = normalizedResponse.products.map((product)=>mapListProduct(product, context));
return {
...normalizedResponse,
products
};
}
};
}
export { createMedusaCatalogService };