UNPKG

@techsio/storefront-data

Version:

Medusa-first storefront data layer built on TanStack Query.

156 lines (155 loc) • 6.55 kB
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 };