Vue Storefront is now Alokai! Learn More
Product normalizers

Product normalizers

Product includes two normalizers:

  • normalizeProduct: This function is used to map Commercetools Product into SfProduct, which includes a full product details
  • normalizeProductCatalogItem: This function is used to map Commercetools 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
productProductCommercetools Product

normalizeProductCatalogItem

NameTypeDefault valueDescription
contextNormalizeProductCatalogItemContextContext needed for the normalizer.
productProductProjection or ProductCommercetools 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 with type field which is available on Commercetools Product and normalizeProductCatalogItem with version field.

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

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

Please keep in mind that the normalizeProductCatalogItem takes ProductProjection or Product as a first argument, so you may use Typescript's type guards to narrow the type.

Source

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

product.ts
import type { NormalizerContext } from "@/normalizers/types";
import { maybe, slugify } from "@shared/utils";
import type {
  Product,
  ProductVariant,
  RawProductAttribute,
} from "@vsf-enterprise/commercetools-types";
import type { SfProduct } from "@vue-storefront/unified-data-model";
import sanitizeHtml from "sanitize-html";
import { defineNormalizer } from "../defineNormalizer";
import { createSfImages } from "./images";

export const normalizeProduct = defineNormalizer.normalizeProduct((context, product) => {
  const {
    masterData: { current },
  } = product;
  const currentVariant = getVariant(product, context.sku);
  const { normalizeDiscountablePrice, normalizeRating } = context.normalizers;

  const { primaryImage, gallery } = createSfImages(context, currentVariant.images);
  const price = currentVariant.price ? normalizeDiscountablePrice(currentVariant.price) : null;
  const rating = product?.reviewRatingStatistics
    ? normalizeRating(product.reviewRatingStatistics)
    : null;
  const variants = normalizeVariants(context, current?.allVariants);
  const attributes = getAttributes(context, currentVariant.attributesRaw);

  return {
    id: product.id,
    sku: maybe(currentVariant?.sku),
    name: maybe(current?.name),
    slug: current?.slug || slugify(product.id, current?.name ?? ""),
    description: current?.description ? sanitizeHtml(current.description) : null,
    price,
    primaryImage,
    gallery,
    rating,
    variants,
    attributes,
    quantityLimit: maybe(currentVariant?.availability?.noChannel?.availableQuantity),
  };
});

function getVariant(product: Product, sku?: string): ProductVariant {
  const {
    masterData: { current },
  } = product;
  const allVariants = current?.allVariants ?? [];

  let currentVariant = current!.masterVariant;
  if (sku != null) {
    currentVariant = allVariants.find((variant) => variant?.sku === sku) ?? currentVariant;
  }

  return currentVariant;
}

function getAttributes(context: NormalizerContext, attributesRaw: RawProductAttribute[]) {
  return attributesRaw.map((attr) => context.normalizers.normalizeAttribute(attr)).filter(Boolean);
}

function normalizeVariants(
  context: NormalizerContext,
  allVariants: Array<ProductVariant> | undefined,
): SfProduct["variants"] {
  if (!allVariants) {
    return [];
  }

  return allVariants.map((variant) => ({
    id: variant.id.toString(),
    sku: maybe(variant?.sku),
    name: null,
    slug: slugify(variant?.sku ?? variant.id.toString()),
    quantityLimit: maybe(variant?.availability?.noChannel?.availableQuantity),
    attributes: getAttributes(context, variant.attributesRaw),
  }));
}