Alokai
ReferenceMagentoNormalizers

Product normalizers

Product includes two normalizers:

  • normalizeProduct: This function is used to map Magento Product into SfProduct, which includes a full product details
  • normalizeProductCatalogItem: This function is used to map Magento Product into SfProductCatalogItem, which includes only basic product details, needed to display a product in a product catalog

Parameters

normalizeProduct

NameTypeDefault valueDescription
contextNormalizeProductContextContext needed for the normalizer. sku is added to specify a product variant
productProductWithTypeName ProductInterface with __typenameMagento Product

normalizeProductCatalogItem

NameTypeDefault valueDescription
contextNormalizerContextcontext needed for the normalizer
productProductWithTypeName ProductInterface with __typenameMagento Product

Extending

The SfProduct model is returned from GetProductDetails method. The SfProductCatalogItem model is returned from GetProducts method. If any of these models don't contain the information you need for your Storefront, you can extend its logic using the addCustomFields API. In the following example we extend the normalizeProduct and normalizeProductCatalogItem with countryOfManufacture field which is available on Magento Product.

import { normalizers } from "@vsf-enterprise/unified-api-magento";

export const unifiedApiExtension = createUnifiedExtension({
  normalizers: {
    addCustomFields: [
      {
        normalizeProduct: (context, product) => {
          const { quantityLimit } = normalizers.normalizeProduct(context, product);
          
          return {
            countryOfManufacture: product.country_of_manufacture,
            isSmallQuantityLimit: quantityLimit && quantityLimit <= 10,
          }
        },
        normalizeProductCatalogItem: (context, product) => ({
          countryOfManufacture: product.country_of_manufacture,
        }),
      },
    ],
  },
  config: {
    ...
  },
});

Source

The normalizeProduct and normalizeProductCatalogItem function consists of several smaller normalizers such as normalizeImage, normalizeRating normalizeDiscountablePrice, and more, which you can override as well.

import sanitizeHtml from "sanitize-html";

import type { ConfigurableVariant, ProductWithTypeName } from "@vsf-enterprise/unified-api-magento/ecommerceTypes";
import { isConfigurableProduct } from "@vsf-enterprise/unified-api-magento/ecommerceTypes";

import { defineNormalizer } from "../defineNormalizer";
import type { NormalizerContext, NormalizersTypes } from "../types";
import { normalizeProductInterface } from "./productCatalog";
import { normalizeProductVariant } from "./variant";

export const normalizeProduct = defineNormalizer.normalizeProduct((context, product) => {
const currentVariant = getVariant(product, context.sku);
const description = product.description?.html ? sanitizeHtml(product.description.html) : null;
const gallery =
  currentVariant.product?.media_gallery
    ?.filter(Boolean)
    .map((img) => context.normalizers.normalizeImage(img)) ?? [];

return {
  ...normalizeProductInterface(context, product, currentVariant?.product),
  attributes: getAttributes(context, product, currentVariant),
  description,
  gallery,
  variants: getVariants(context, product),
};
});

function getAttributes(
context: NormalizerContext,
product: ProductWithTypeName,
currentVariant: { attributes: ConfigurableVariant["attributes"] },
) {
if (!isConfigurableProduct(product)) {
  return [];
}
const configurableOptions = (product.configurable_options ?? []).filter(Boolean);
const attributes = currentVariant.attributes;
if (!(attributes && configurableOptions?.length > 0)) {
  return [];
}

return attributes
  .filter(Boolean)
  .map((attributeOption) =>
    context.normalizers.normalizeAttribute({ attributeOption, configurableOptions }),
  )
  .filter(Boolean);
}

function getVariant(product: ProductWithTypeName, sku?: string) {
if (!isConfigurableProduct(product)) {
  return { attributes: [], product };
}
const variant = product.variants?.find((variant) => variant?.product?.sku === sku);
if (!variant?.product) {
  return { attributes: [], product };
}
return { attributes: variant.attributes?.filter(Boolean) ?? [], product: variant.product };
}

function getVariants(context: NormalizerContext, product: NormalizersTypes.NormalizeProductInput) {
if (isConfigurableProduct(product)) {
  const configurable_options = (product.configurable_options ?? []).filter(Boolean);
  return (product.variants ?? [])
    .filter(Boolean)
    .map((variant) => normalizeProductVariant(context, variant, configurable_options));
}
return [];
}
import type { SfProductCatalogItem } from "@alokai/connect";

import { isConfigurableProduct, type ProductInterface } from "@vsf-enterprise/unified-api-magento/ecommerceTypes";
import { maybe, slugify } from "@vsf-enterprise/unified-api-magento";

import { defineNormalizer } from "../defineNormalizer";
import type { NormalizerContext } from "../types";

export const normalizeProductCatalogItem = defineNormalizer.normalizeProductCatalogItem(
(context, product) => {
  if (isConfigurableProduct(product)) {
    // use the first variant as the main purchasable product or fallback to product
    const variant = product.variants?.[0]?.product ?? product;
    return normalizeProductInterface(context, product, variant);
  }
  return normalizeProductInterface(context, product, product);
},
);

export function normalizeProductInterface(
context: NormalizerContext,
product: ProductInterface,
variant: ProductInterface,
): SfProductCatalogItem {
const { normalizeDiscountablePrice, normalizeImage, normalizeRating } = context.normalizers;

return {
  id: product.sku!,
  name: maybe(variant.name),
  price: variant?.price_range?.minimum_price
    ? normalizeDiscountablePrice(variant.price_range.minimum_price)
    : null,
  primaryImage: variant.thumbnail ? normalizeImage(variant.thumbnail) : null,
  quantityLimit: maybe(variant.only_x_left_in_stock),
  rating: normalizeRating(product),
  sku: maybe(variant.sku),
  slug: product.url_key || slugify(product.name ?? ""),
};
}
import { normalize } from "@vsf-enterprise/unified-api-magento/ecommerceTypes";

import { defineNormalizer } from "../defineNormalizer";

export const normalizeRating = defineNormalizer.normalizeRating((_context, productData) => {
if (productData.rating_summary === undefined || productData.review_count === undefined) {
  return null;
}
return {
  average: normalizeRatingSummary(productData.rating_summary),
  count: productData.review_count,
};
});

/*
* Normalize rating summary from 0-100 to 0-5
*/
function normalizeRatingSummary(value: number): number {
return normalize(value, 0, 100, 0, 5);
}

On this page