import type { Product, ProductImage, ProductPrice, ProductVariant, } from '@vercel/commerce/types/product' import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product' import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships' import { jsonApi } from '@spree/storefront-api-v2-sdk' import { JsonApiDocument } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import { requireConfigValue } from '../../isomorphic-config' import createGetAbsoluteImageUrl from '../create-get-absolute-image-url' import expandOptions from '../expand-options' import getMediaGallery from '../get-media-gallery' import getProductPath from '../get-product-path' import MissingPrimaryVariantError from '../../errors/MissingPrimaryVariantError' import MissingOptionValueError from '../../errors/MissingOptionValueError' import type { ExpandedProductOption, SpreeSdkResponse, VariantAttr, } from '../../types' const placeholderImage = requireConfigValue('productPlaceholderImageUrl') as | string | false const imagesOptionFilter = requireConfigValue('imagesOptionFilter') as | string | false const normalizeProduct = ( spreeSuccessResponse: SpreeSdkResponse, spreeProduct: ProductAttr ): Product => { const spreePrimaryVariant = jsonApi.findSingleRelationshipDocument( spreeSuccessResponse, spreeProduct, 'primary_variant' ) if (spreePrimaryVariant === null) { throw new MissingPrimaryVariantError( `Couldn't find primary variant for product with id ${spreeProduct.id}.` ) } const sku = spreePrimaryVariant.attributes.sku const price: ProductPrice = { value: parseFloat(spreeProduct.attributes.price), currencyCode: spreeProduct.attributes.currency, } const hasNonMasterVariants = (spreeProduct.relationships.variants.data as RelationType[]).length > 1 const showOptions = (requireConfigValue('showSingleVariantOptions') as boolean) || hasNonMasterVariants let options: ExpandedProductOption[] = [] const spreeVariantRecords = jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreeProduct, 'variants' ) // Use variants with option values if available. Fall back to // Spree primary_variant if no explicit variants are present. const spreeOptionsVariantsOrPrimary = spreeVariantRecords.length === 0 ? [spreePrimaryVariant] : spreeVariantRecords const variants: ProductVariant[] = spreeOptionsVariantsOrPrimary.map( (spreeVariantRecord) => { let variantOptions: ExpandedProductOption[] = [] if (showOptions) { const spreeOptionValues = jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreeVariantRecord, 'option_values' ) // Only include options which are used by variants. spreeOptionValues.forEach((spreeOptionValue) => { variantOptions = expandOptions( spreeSuccessResponse, spreeOptionValue, variantOptions ) options = expandOptions( spreeSuccessResponse, spreeOptionValue, options ) }) } return { id: spreeVariantRecord.id, options: variantOptions, } } ) const spreePrimaryVariantImageRecords = jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreePrimaryVariant, 'images' ) let spreeVariantImageRecords: JsonApiDocument[] if (imagesOptionFilter === false) { spreeVariantImageRecords = spreeVariantRecords.reduce( (accumulatedImageRecords, spreeVariantRecord) => { return [ ...accumulatedImageRecords, ...jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreeVariantRecord, 'images' ), ] }, [] ) } else { const spreeOptionTypes = jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreeProduct, 'option_types' ) const imagesFilterOptionType = spreeOptionTypes.find( (spreeOptionType) => spreeOptionType.attributes.name === imagesOptionFilter ) if (!imagesFilterOptionType) { console.warn( `Couldn't find option type having name ${imagesOptionFilter} for product with id ${spreeProduct.id}.` + ' Showing no images for this product.' ) spreeVariantImageRecords = [] } else { const imagesOptionTypeFilterId = imagesFilterOptionType.id const includedOptionValuesImagesIds: string[] = [] spreeVariantImageRecords = spreeVariantRecords.reduce( (accumulatedImageRecords, spreeVariantRecord) => { const spreeVariantOptionValuesIdentifiers: RelationType[] = spreeVariantRecord.relationships.option_values.data const spreeOptionValueOfFilterTypeIdentifier = spreeVariantOptionValuesIdentifiers.find( (spreeVariantOptionValuesIdentifier: RelationType) => imagesFilterOptionType.relationships.option_values.data.some( (filterOptionTypeValueIdentifier: RelationType) => filterOptionTypeValueIdentifier.id === spreeVariantOptionValuesIdentifier.id ) ) if (!spreeOptionValueOfFilterTypeIdentifier) { throw new MissingOptionValueError( `Couldn't find option value related to option type with id ${imagesOptionTypeFilterId}.` ) } const optionValueImagesAlreadyIncluded = includedOptionValuesImagesIds.includes( spreeOptionValueOfFilterTypeIdentifier.id ) if (optionValueImagesAlreadyIncluded) { return accumulatedImageRecords } includedOptionValuesImagesIds.push( spreeOptionValueOfFilterTypeIdentifier.id ) return [ ...accumulatedImageRecords, ...jsonApi.findRelationshipDocuments( spreeSuccessResponse, spreeVariantRecord, 'images' ), ] }, [] ) } } const spreeImageRecords = [ ...spreePrimaryVariantImageRecords, ...spreeVariantImageRecords, ] const productImages = getMediaGallery( spreeImageRecords, createGetAbsoluteImageUrl(requireConfigValue('imageHost') as string) ) const images: ProductImage[] = productImages.length === 0 ? placeholderImage === false ? [] : [{ url: placeholderImage }] : productImages const slug = spreeProduct.attributes.slug const path = getProductPath(spreeProduct) return { id: spreeProduct.id, name: spreeProduct.attributes.name, description: spreeProduct.attributes.description, images, variants, options, price, slug, path, sku, } } export default normalizeProduct