Facet normalizer
Concept of facets exists in both Unified Data Layer world and SAP. The normalizeFacet
function maps SAP Facet
into Unified SfFacet
.
Parameters
Name | Type | Default value | Description |
---|---|---|---|
context | NormalizerContext | Context which contains currentQuery for searchProducts | |
facet | Facet | SAP Facet |
Extending
The SfFacet
is returned from SearchProducts
Method. If the SfFacet
structure doesn't contain the information you need for your Storefront, you can extend its logic using the addCustomFields
API. The following example demonstrates how to extend SfFacet
with a multiSelect
field.
export const unifiedApiExtension = createUnifiedExtension({
normalizers: {
addCustomFields: [
{
normalizeFacet: (context, facet) => ({
multiSelect: facet.multiSelect,
}),
},
],
},
config: {
...
},
});
Source
In the Unified Data Layer, the SfFacet
array is expected to return all of the available facets, including the current selected facets. In SAP the selected facets are present in the currentQuery
object returned from the searchProducts
API Client method. So to retrieve them, we have to extract them from the query value. An explanation of the expected behaviour can be found in the facet.feature
file which contains test scenarios.
import { maybe } from "@/helpers";
import type { Facet, FacetValue } from "@vsf-enterprise/sapcc-types";
import { SfFacetTypes, type SfFacetType } from "@vue-storefront/unified-data-model";
import { defineNormalizer } from "../defineNormalizer";
export const normalizeFacet = defineNormalizer.normalizeFacet((context, facet) => {
const { name: label, values } = facet;
const name = normalizeFacetName(facet);
const selectedValues = getSelectedValues(context.currentQuery?.query?.value ?? "", name);
const sfFacetItems =
values?.map((term) => ({
label: term.name as string,
value: normalizeFacetItemValue(term, selectedValues),
productCount: maybe(term.count),
})) || [];
if (sfFacetItems.length === 0) {
return null;
}
const { getFacetType = defaultGetFacetType } = context;
return {
label: label as string,
name: normalizeFacetName(facet),
values: sfFacetItems,
type: getFacetType(facet),
};
});
function normalizeFacetName(facet: Facet): string {
let name = facet.name as string;
const nonSelectedValue = facet.values?.find((value) => !value?.selected);
const exampleQueryUrl = nonSelectedValue?.query?.query?.value;
if (exampleQueryUrl) {
name = exampleQueryUrl.split(":").at(-2) ?? name;
}
return name;
}
function normalizeFacetItemValue(facetValue: FacetValue, selectedValues: string[]): string {
let value = facetValue.name as string;
const queryUrl = facetValue?.query?.query?.value;
if (facetValue.selected) {
value = guessSelectedFacetItemValue(value, selectedValues);
} else if (queryUrl) {
value = queryUrl.split(":").at(-1) ?? value;
}
return value;
}
function getSelectedValues(queryValue: string, facetName: string): string[] {
const regex = new RegExp(`:${facetName}:([^:]+)`, "g");
const matches = queryValue.match(regex);
if (matches) {
return matches.map((match) => match.split(":")?.[2]).filter(Boolean);
}
return [];
}
function guessSelectedFacetItemValue(facetItemName: string, selectedValues: string[]): string {
const facetNameAsUri = encodeURIComponent(facetItemName as string).replace(/%20/g, "+");
return (
selectedValues.find((value) => value.toLowerCase() === facetNameAsUri.toLowerCase()) ??
facetNameAsUri.toUpperCase()
);
}
function defaultGetFacetType(facet: Facet): SfFacetType {
return facet.multiSelect === false ? SfFacetTypes.SINGLE_SELECT : SfFacetTypes.MULTI_SELECT;
}