SearchProducts
Implements SearchProducts
Unified Method.
Source
// Splitting this functions into smaller ones actually may make it less readable
// in this scenario
/* eslint-disable max-lines-per-function */
import { defineApi } from "@vsf-enterprise/unified-api-sfcc";
import type { SFCCIntegrationContext } from "@vsf-enterprise/sfcc-api";
import type { ProductSearchResult } from "@vsf-enterprise/sfcc-types";
import {
type SearchProducts,
type SearchProductsArgs,
getNormalizers,
} from "@vsf-enterprise/unified-api-sfcc/udl";
import type { Search } from "commerce-sdk";
import { findRefinementCategory, mapFacetsToRefinements, mapSort } from "@vsf-enterprise/unified-api-sfcc";
declare module "@vsf-enterprise/unified-api-sfcc" {
interface SearchProductsExtendedArgs {}
}
const CATEGORY_REFINEMENT_ID = "cgid";
export const searchProducts = defineApi.searchProducts(async (context, args) => {
try {
return await searchProductsByQuery(context, args);
} catch (error) {
console.error("/searchProducts", error);
return {
products: [],
pagination: {
currentPage: 0,
pageSize: 0,
totalResults: 0,
totalPages: 0,
},
facets: [],
};
}
});
async function searchProductsByQuery(
context: SFCCIntegrationContext,
{ search, sortBy, facets: facetsQuery, category, currentPage = 1, pageSize }: SearchProductsArgs,
): ReturnType<SearchProducts> {
const { filterFacets = () => true, rootCategoryId = "root" } = context.config.unified;
const shouldReturnWholeCatalog = !category;
// if no params are passed then searchProduct returns no results
// UDM predicts that in case of no params we should return all results
// root is the main category in sfcc so we effectively query all products here
// in case when no params are passed
const categoryToSearchBy = shouldReturnWholeCatalog ? rootCategoryId : category;
// UST-706: we temporarily don't support sets, bundles and variation groups
// Note: Salesforce TsDocs claim that the correct refinement to filter by the
// product type is `htypes` but it doesn't work when actually attempting to use it.
// `htype` works fine though.
const SUPPORTED_PRODUCT_TYPES = ["master", "product"];
// TS thinks that categorRefinement may be { cgid: undefined | string}
// So we cast it to a Record<string, string>
const categoryRefinement: Record<string, string> = categoryToSearchBy
? { cgid: categoryToSearchBy }
: {};
const facets = {
htype: SUPPORTED_PRODUCT_TYPES,
...categoryRefinement,
...facetsQuery,
};
const paginationOffset = Math.max(currentPage - 1, 0) * (pageSize ?? 0);
const refine = mapFacetsToRefinements(facets);
const searchProductsResults = await context.api.searchProducts({
q: search,
limit: pageSize,
offset: paginationOffset,
refine,
sort: mapSort(sortBy),
});
const productsHits = searchProductsResults.hits ?? [];
const { normalizePagination, normalizeFacet, normalizeProductCatalogItem } =
getNormalizers(context);
const productIds = productsHits.map(
(product) => product?.representedProduct?.id ?? product.productId,
);
// The maximum number of productIDs that can be requested in the getProducts() method are 24.
// https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-products?meta=getProducts
const promises = chunk(productIds, 24).map((ids) => {
return context.api.getProducts({
ids: ids,
perPricebook: true,
});
});
const productsDetails = (await Promise.all(promises)).flat();
const sfProductCatalogItems = productsHits
.map((product, index) => ({
...product,
tieredPrices: productsDetails.at(index)?.tieredPrices,
}))
.map((product) => normalizeProductCatalogItem(product));
const paginationNormaliserParams = {
limit: pageSize ?? searchProductsResults.limit,
offset: searchProductsResults.offset,
total: searchProductsResults.total,
} satisfies Pick<Search.ShopperSearch.SimpleSearchResult, "limit" | "offset" | "total">;
const sfPagination = normalizePagination(paginationNormaliserParams);
const sfFacets = searchProductsResults.refinements
.filter((refinement) => {
// We filter out 'htype' to let through only supported products. Other products are not supported
// and we do not let user request them here.
const isSystemFilter = ["htype"].includes(refinement.attributeId);
return !isSystemFilter;
})
.filter((refinement) => filterFacets(refinement))
.map((refinement) =>
transformCategoryRefinement(
refinement,
searchProductsResults.selectedRefinements,
rootCategoryId,
),
)
.map((refinement) => normalizeFacet(refinement))
.filter(Boolean);
return {
products: sfProductCatalogItems,
pagination: sfPagination,
facets: sfFacets,
};
}
function transformCategoryRefinement(
refinement: ProductSearchResult["refinements"][number],
selectedRefinements: ProductSearchResult["selectedRefinements"],
rootCategoryId: string,
) {
if (refinement.attributeId !== CATEGORY_REFINEMENT_ID) {
return refinement;
}
const refinedCategory = selectedRefinements?.[CATEGORY_REFINEMENT_ID];
if (refinedCategory === rootCategoryId) {
return refinement;
}
const currentCategory = findRefinementCategory(refinement.values ?? [], refinedCategory);
if (currentCategory) {
return {
...refinement,
values: currentCategory?.values ?? [],
};
}
return refinement;
}
function chunk<T>(input: T[], size: number) {
return [...Array(Math.ceil(input.length / size))].map((_, i) =>
input.slice(size * i, size + size * i),
);
}