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 { type SfFacetType, SfFacetTypes } from "@alokai/connect";
import type { Facet, FacetValue } from "@vsf-enterprise/sapcc-types";
import { maybe } from "@vsf-enterprise/unified-api-sapcc";
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,
productCount: maybe(term.count),
value: normalizeFacetItemValue(term, selectedValues),
})) || [];
if (sfFacetItems.length === 0) {
return null;
}
const { getFacetType = defaultGetFacetType } = context;
return {
label: label as string,
name: normalizeFacetName(facet),
type: getFacetType(facet),
values: sfFacetItems,
};
});
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, "+");
const nameLower = facetNameAsUri.toLowerCase();
const exactMatch = selectedValues.find((value) => value.toLowerCase() === nameLower);
if (exactMatch) return exactMatch;
const suffixMatches = selectedValues.filter((value) =>
value.toLowerCase().endsWith(`-${nameLower}`),
);
if (suffixMatches.length > 0) {
return suffixMatches.sort((a, b) => a.length - b.length)[0]!;
}
return facetNameAsUri.toUpperCase();
}
function defaultGetFacetType(facet: Facet): SfFacetType {
return facet.multiSelect === false ? SfFacetTypes.SINGLE_SELECT : SfFacetTypes.MULTI_SELECT;
}Feature: Normalize SfFacet
Scenario: Normalizing a valid Facet object
Given a valid Facet object with values
When the normalize function is called
Then the returned SfFacet object should have the same label and name as the Facet object
And the SfFacet object should have an array of SfFacetItem objects that match the values in the FacetValue object
Scenario: Normalizing a Facet object without values
Given a Facet object without values
When the normalize function is called
Then the returned null
Scenario: Normalizing a Facet object with undefined values
Given a Facet object with undefined values
When the normalize function is called
Then the returned null
Scenario: Normalizing a Facet object with an empty string as the facet name
Given a Facet object with an empty string as the facet name
When the normalize function is called
Then the returned SfFacet object should have an empty string as the label and name
And the SfFacet object should have an array of SfFacetItem objects that match the terms in the FacetValue object
Scenario Outline: Facet has a value which has query url and is not selected
Given a Facet object with a name <name> and one value
And value has query value of <queryValue>
When the normalize function is called
Then the returned SfFacet object should have the name equal to <expectedName>
And the facet item should have value equal to <expectedValue>
Examples:
| name | queryValue | expectedName | expectedValue |
| Colour | :relevance:allCategories:collections:swatchColors:BLACK | swatchColors | BLACK |
| Size | :relevance:allCategories:collections:size:4.0+US | size | 4.0+US |
| Brand | :relevance:allCategories:collections:brand:Burton | brand | Burton |
Scenario Outline: Facet has a value which has query url and is selected
Given a Facet object with two values and one of the value is selected
And current query value is <currentQueryValue>
And selected value has query value of <selectedValueQuery>
And selected value has name of <selectedValueName>
And non selected value has query value of <nonselectedValueQuery>
When the normalize function is called
Then the selected facet item should have value equal to <expectedSelectedValue>
Examples:
| currentQueryValue | nonselectedValueQuery | selectedValueQuery | selectedValueName | expectedSelectedValue |
| :swatchColors:RED | :swatchColors:RED:swatchColors:GRAY | | Red | RED |
| :brand:Burton:swatchColors:RED:swatchColors:GREY | :swatchColors:RED:swatchColors:GREY:brand:Volcom | :swatchColors:RED:swatchColors:GREY | Burton | Burton |
| :swatchColors:RED:size:8.0+US | :swatchColors:RED:size:8.0+US:size:7.0 | :swatchColors:RED | 8.0 US | 8.0+US |
| :swatchColors:color-black | :swatchColors:color-black:swatchColors:color-blue | | Black | color-black |
| :swatchColors:color-black:size:size-10 | :swatchColors:color-black:size:size-10:size:size-8 | :swatchColors:color-black | 10 | size-10 |
Scenario Outline: Normalizing a Facet should map default facet type
Given a valid Facet object with multiselect set to <multiselect>
When the normalize function is called
Then the returned type equals <expectedType>
Examples:
| multiselect | expectedType |
| true | MULTI_SELECT |
| false | SINGLE_SELECT |
| undefined | MULTI_SELECT |
Scenario: Using a custom getFacetType function
Given a valid Facet object
And a custom getFacetType function
When the normalize function is called
Then the returned type equals custom type