{"version":3,"file":"getProductOptions.mjs","names":[],"sources":["../../src/getProductOptions.ts"],"sourcesContent":["import {isOptionValueCombinationInEncodedVariant} from './optionValueDecoder.js';\nimport type {\n  Product,\n  ProductOption,\n  ProductOptionValue,\n  ProductVariant,\n  SelectedOption,\n} from './storefront-api-types';\n\nexport type RecursivePartial<T> = {\n  [P in keyof T]?: RecursivePartial<T[P]>;\n};\ntype ProductOptionsMapping = Record<string, Record<string, number>>;\ntype ProductOptionValueState = {\n  variant: ProductVariant;\n  handle: string;\n  variantUriQuery: string;\n  selected: boolean;\n  exists: boolean;\n  available: boolean;\n  isDifferentProduct: boolean;\n};\ntype MappedProductOptionValue = ProductOptionValue & ProductOptionValueState;\n\n/**\n * Creates a mapping of product options to their index for matching encoded values\n * For example, a product option of\n *  [\n *    \\{\n *      name: 'Color',\n *      optionValues: [\\{name: 'Red'\\}, \\{name: 'Blue'\\}]\n *    \\},\n *    \\{\n *      name: 'Size',\n *      optionValues: [\\{name: 'Small'\\}, \\{name: 'Medium'\\}, \\{name: 'Large'\\}]\n *    \\}\n *  ]\n * Would return\n *  \\{\n *    'Color': \\{Red: 0, Blue: 1\\},\n *    'Size': \\{Small: 0, Medium: 1, Large: 2\\}\n *  \\}\n */\nfunction mapProductOptions(options: ProductOption[]): ProductOptionsMapping {\n  return Object.assign(\n    {},\n    ...options.map((option: ProductOption) => {\n      return {\n        [option.name]: Object.assign(\n          {},\n          ...(option?.optionValues\n            ? option.optionValues.map((value, index) => {\n                return {[value.name]: index};\n              })\n            : []),\n        ),\n      } as Record<string, number>;\n    }),\n  );\n}\n\n/**\n * Converts the product option into an Object\\<key, value\\> for building query params\n * For example, a selected product option of\n *  [\n *    \\{\n *      name: 'Color',\n *      value: 'Red',\n *    \\},\n *    \\{\n *      name: 'Size',\n *      value: 'Medium',\n *    \\}\n *  ]\n * Would return\n *  \\{\n *    Color: 'Red',\n *    Size: 'Medium',\n *  \\}\n * @publicDocs\n */\nexport function mapSelectedProductOptionToObject(\n  options: Pick<SelectedOption, 'name' | 'value'>[],\n): Record<string, string> {\n  return Object.assign(\n    {},\n    ...options.map((key) => {\n      return {[key.name]: key.value};\n    }),\n  ) as Record<string, string>;\n}\n\n/**\n * Returns the JSON stringify result of mapSelectedProductOptionToObject\n */\nfunction mapSelectedProductOptionToObjectAsString(\n  options: Pick<SelectedOption, 'name' | 'value'>[],\n): string {\n  return JSON.stringify(mapSelectedProductOptionToObject(options));\n}\n\n/**\n * Encode the selected product option as a key for mapping to the encoded variants\n * For example, a selected product option of\n *  [\n *    \\{\n *      name: 'Color',\n *      value: 'Red',\n *    \\},\n *    \\{\n *      name: 'Size',\n *      value: 'Medium',\n *    \\}\n *  ]\n * Would return\n *  JSON.stringify(\\{\n *    Color: 'Red',\n *    Size: 'Medium',\n *  \\})\n */\nfunction encodeSelectedProductOptionAsKey(\n  selectedOption:\n    | Pick<SelectedOption, 'name' | 'value'>[]\n    | Record<string, string>,\n): string {\n  if (Array.isArray(selectedOption)) {\n    return JSON.stringify(\n      Object.assign(\n        {},\n        ...selectedOption.map((option) => ({[option.name]: option.value})),\n      ),\n    );\n  } else {\n    return JSON.stringify(selectedOption);\n  }\n}\n\n/**\n * Build the encoding array for the given selected options. For example, if we have\n * the following productOptionMappings:\n *\n *  \\{\n *    'Color': \\{Red: 0, Blue: 1\\},\n *    'Size': \\{Small: 0, Medium: 1, Large: 2\\}\n *  \\}\n *\n * A selectedOption of\n *\n * \\{\n *    Color: 'Red',\n *    Size: 'Medium',\n * \\}\n *\n * `buildEncodingArrayFromSelectedOptions` will produce\n *\n * [0,1]\n *\n * If in the case where a selected option doesn't exists in the mapping array, for example:\n *\n * \\{\n *    Color: 'Red',\n *    Fabric: 'Cotton',\n *    Size: 'Medium',\n * \\}\n *\n * `buildEncodingArrayFromSelectedOptions` will still produce\n *\n *  [0,1]\n *\n * This can be caused by when we do not have all the product\n * option information for the loading optimistic variant\n */\nfunction buildEncodingArrayFromSelectedOptions(\n  selectedOption: Record<string, string>,\n  productOptionMappings: ProductOptionsMapping,\n): Array<number> {\n  const encoding = Object.keys(selectedOption).map((key) => {\n    return productOptionMappings[key]\n      ? productOptionMappings[key][selectedOption[key]]\n      : null;\n  });\n  return encoding.filter((code) => code !== null);\n}\n\n/**\n * Takes an array of product variants and maps them to an object with the encoded selected option values as the key.\n * For example, a product variant of\n * [\n *  \\{\n *    id: 1,\n *    selectedOptions: [\n *      \\{name: 'Color', value: 'Red'\\},\n *      \\{name: 'Size', value: 'Small'\\},\n *    ],\n *  \\},\n *  \\{\n *    id: 2,\n *    selectedOptions: [\n *      \\{name: 'Color', value: 'Red'\\},\n *      \\{name: 'Size', value: 'Medium'\\},\n *    ],\n *  \\}\n * ]\n * Would return\n * \\{\n *    '[0,0]': \\{id: 1, selectedOptions: [\\{name: 'Color', value: 'Red'\\}, \\{name: 'Size', value: 'Small'\\}]\\},\n *    '[0,1]': \\{id: 2, selectedOptions: [\\{name: 'Color', value: 'Red'\\}, \\{name: 'Size', value: 'Medium'\\}]\\},\n * \\}\n */\nfunction mapVariants(\n  variants: ProductVariant[],\n): Record<string, ProductVariant> {\n  return Object.assign(\n    {},\n    ...variants.map((variant) => {\n      const variantKey = encodeSelectedProductOptionAsKey(\n        variant.selectedOptions || [],\n      );\n      return {[variantKey]: variant};\n    }),\n  ) as Record<string, ProductVariant>;\n}\n\nexport type MappedProductOptions = Omit<ProductOption, 'optionValues'> & {\n  optionValues: MappedProductOptionValue[];\n};\n\nconst PRODUCT_INPUTS = [\n  'options',\n  'selectedOrFirstAvailableVariant',\n  'adjacentVariants',\n];\n\nconst PRODUCT_INPUTS_EXTRA = [\n  'handle',\n  'encodedVariantExistence',\n  'encodedVariantAvailability',\n];\n\nfunction logErrorAndReturnFalse(key: string): boolean {\n  console.error(\n    `[h2:error:getProductOptions] product.${key} is missing. Make sure you query for this field from the Storefront API.`,\n  );\n  return false;\n}\n\nexport function checkProductParam(\n  product: RecursivePartial<Product>,\n  checkAll = false,\n): Product {\n  let validParam = true;\n  const productKeys = Object.keys(product);\n\n  // Check product input\n  (checkAll\n    ? [...PRODUCT_INPUTS, ...PRODUCT_INPUTS_EXTRA]\n    : PRODUCT_INPUTS\n  ).forEach((key) => {\n    if (!productKeys.includes(key)) {\n      validParam = logErrorAndReturnFalse(key);\n    }\n  });\n\n  // Check for nested options requirements\n  if (product.options) {\n    const firstOption = product?.options[0];\n\n    if (checkAll && !firstOption?.name) {\n      validParam = logErrorAndReturnFalse('options.name');\n    }\n\n    // Check for options.optionValues\n    if (product?.options[0]?.optionValues) {\n      let firstOptionValues = product.options[0].optionValues[0];\n\n      // Check for options.optionValues.name\n      if (checkAll && !firstOptionValues?.name) {\n        validParam = logErrorAndReturnFalse('options.optionValues.name');\n      }\n\n      // It is possible for firstSelectableVariant to be null\n      firstOptionValues = product.options[0].optionValues.filter(\n        (value) => !!value?.firstSelectableVariant,\n      )[0];\n\n      // Check for options.optionValues.firstSelectableVariant\n      if (firstOptionValues?.firstSelectableVariant) {\n        // check product variant\n        validParam = checkProductVariantParam(\n          firstOptionValues.firstSelectableVariant,\n          'options.optionValues.firstSelectableVariant',\n          validParam,\n          checkAll,\n        );\n      }\n    } else {\n      validParam = logErrorAndReturnFalse('options.optionValues');\n    }\n  }\n\n  // Check for nested selectedOrFirstAvailableVariant requirements\n  if (product.selectedOrFirstAvailableVariant) {\n    validParam = checkProductVariantParam(\n      product.selectedOrFirstAvailableVariant,\n      'selectedOrFirstAvailableVariant',\n      validParam,\n      checkAll,\n    );\n  }\n\n  // Check for nested adjacentVariants requirements\n  if (!!product.adjacentVariants && product.adjacentVariants[0]) {\n    validParam = checkProductVariantParam(\n      product.adjacentVariants[0],\n      'adjacentVariants',\n      validParam,\n      checkAll,\n    );\n  }\n\n  return (validParam ? product : {}) as Product;\n}\n\nfunction checkProductVariantParam(\n  variant: RecursivePartial<ProductVariant>,\n  key: string,\n  currentValidParamState: boolean,\n  checkAll: boolean,\n): boolean {\n  let validParam = currentValidParamState;\n\n  if (checkAll && !variant.product?.handle) {\n    validParam = logErrorAndReturnFalse(`${key}.product.handle`);\n  }\n  if (variant.selectedOptions) {\n    const firstSelectedOption = variant.selectedOptions[0];\n    if (!firstSelectedOption?.name) {\n      validParam = logErrorAndReturnFalse(`${key}.selectedOptions.name`);\n    }\n    if (!firstSelectedOption?.value) {\n      validParam = logErrorAndReturnFalse(`${key}.selectedOptions.value`);\n    }\n  } else {\n    validParam = logErrorAndReturnFalse(`${key}.selectedOptions`);\n  }\n\n  return validParam;\n}\n\n/**\n * Finds all the variants provided by adjacentVariants, options.optionValues.firstAvailableVariant,\n * and selectedOrFirstAvailableVariant and return them in a single array\n * @publicDocs\n */\nexport function getAdjacentAndFirstAvailableVariants(\n  product: RecursivePartial<Product>,\n): ProductVariant[] {\n  // Checks for valid product input\n  const checkedProduct = checkProductParam(product);\n\n  if (!checkedProduct.options) return [];\n\n  const availableVariants: Record<string, ProductVariant> = {};\n  checkedProduct.options.map((option) => {\n    option.optionValues?.map((value) => {\n      if (value.firstSelectableVariant) {\n        const variantKey = mapSelectedProductOptionToObjectAsString(\n          value.firstSelectableVariant.selectedOptions,\n        );\n        availableVariants[variantKey] = value.firstSelectableVariant;\n      }\n    });\n  });\n\n  checkedProduct.adjacentVariants.map((variant) => {\n    const variantKey = mapSelectedProductOptionToObjectAsString(\n      variant.selectedOptions,\n    );\n    availableVariants[variantKey] = variant;\n  });\n\n  const selectedVariant = checkedProduct.selectedOrFirstAvailableVariant;\n  if (selectedVariant) {\n    const variantKey = mapSelectedProductOptionToObjectAsString(\n      selectedVariant.selectedOptions,\n    );\n    availableVariants[variantKey] = selectedVariant;\n  }\n\n  return Object.values(availableVariants);\n}\n\n/**\n * Returns a product options array with its relevant information\n * about the variant\n * @publicDocs\n */\nexport function getProductOptions(\n  product: RecursivePartial<Product>,\n): MappedProductOptions[] {\n  // Checks for valid product input\n  const checkedProduct = checkProductParam(product, true);\n\n  if (!checkedProduct.options) return [];\n\n  const {\n    options,\n    selectedOrFirstAvailableVariant: selectedVariant,\n    adjacentVariants,\n    encodedVariantExistence,\n    encodedVariantAvailability,\n    handle: productHandle,\n  } = checkedProduct;\n\n  // The available product options is dictated by the selected options of the current variant:\n  // Filter out un-used options (Happens on parent combined listing product)\n  const selectedOptionKeys = selectedVariant?.selectedOptions.map(\n    (option) => option.name,\n  );\n  const filteredOptions = options.filter((option) => {\n    return selectedOptionKeys && selectedOptionKeys.indexOf(option.name) >= 0;\n  });\n\n  // Get a mapping of product option names to their index for matching encoded values\n  const productOptionMappings = mapProductOptions(options);\n\n  // Get the adjacent variants mapped to the encoded selected option values\n  const variants = mapVariants(\n    selectedVariant ? [selectedVariant, ...adjacentVariants] : adjacentVariants,\n  );\n\n  // Get the key:value version of selected options for building url query params\n  const selectedOptions = mapSelectedProductOptionToObject(\n    selectedVariant ? selectedVariant.selectedOptions : [],\n  );\n\n  const productOptions = filteredOptions.map((option, optionIndex) => {\n    return {\n      ...option,\n      optionValues: option.optionValues.map((value) => {\n        const targetOptionParams = {...selectedOptions}; // Clones the selected options\n\n        // Modify the selected option value to the current option value\n        targetOptionParams[option.name] = value.name;\n\n        // Encode the new selected option values as a key for mapping to the product variants\n        const targetKey = encodeSelectedProductOptionAsKey(\n          targetOptionParams || [],\n        );\n        const encodingKey = buildEncodingArrayFromSelectedOptions(\n          targetOptionParams || [],\n          productOptionMappings,\n        );\n\n        // Top-down option check for existence and availability\n        const topDownKey = encodingKey.slice(0, optionIndex + 1);\n        const exists = isOptionValueCombinationInEncodedVariant(\n          topDownKey,\n          encodedVariantExistence || '',\n        );\n        const available = isOptionValueCombinationInEncodedVariant(\n          topDownKey,\n          encodedVariantAvailability || '',\n        );\n\n        // Get the variant for the current option value if exists, else use the first selectable variant\n        const variant: ProductVariant =\n          variants[targetKey] || value.firstSelectableVariant;\n\n        // Build the query params for this option value\n        let variantOptionParam = {};\n        if (variant) {\n          variantOptionParam = mapSelectedProductOptionToObject(\n            variant.selectedOptions || [],\n          );\n        }\n        const searchParams = new URLSearchParams(variantOptionParam);\n        const handle = variant?.product?.handle || productHandle;\n\n        return {\n          ...value,\n          variant,\n          handle,\n          variantUriQuery: searchParams.toString(),\n          selected: selectedOptions[option.name] === value.name,\n          exists,\n          available,\n          isDifferentProduct: handle !== productHandle,\n        };\n      }),\n    };\n  });\n\n  return productOptions;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA2CA,SAAS,kBAAkB,SAAiD;AAC1E,QAAO,OAAO,OACZ,EAAE,EACF,GAAG,QAAQ,KAAK,WAA0B;AACxC,SAAO,GACJ,OAAO,OAAO,OAAO,OACpB,EAAE,EACF,GAAI,QAAQ,eACR,OAAO,aAAa,KAAK,OAAO,UAAU;AACxC,UAAO,GAAE,MAAM,OAAO,OAAM;IAC5B,GACF,EAAE,CACP,EACF;GACD,CACH;;;;;;;;;;;;;;;;;;;;;;AAuBH,SAAgB,iCACd,SACwB;AACxB,QAAO,OAAO,OACZ,EAAE,EACF,GAAG,QAAQ,KAAK,QAAQ;AACtB,SAAO,GAAE,IAAI,OAAO,IAAI,OAAM;GAC9B,CACH;;;;;AAMH,SAAS,yCACP,SACQ;AACR,QAAO,KAAK,UAAU,iCAAiC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBlE,SAAS,iCACP,gBAGQ;AACR,KAAI,MAAM,QAAQ,eAAe,CAC/B,QAAO,KAAK,UACV,OAAO,OACL,EAAE,EACF,GAAG,eAAe,KAAK,YAAY,GAAE,OAAO,OAAO,OAAO,OAAM,EAAE,CACnE,CACF;KAED,QAAO,KAAK,UAAU,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCzC,SAAS,sCACP,gBACA,uBACe;AAMf,QALiB,OAAO,KAAK,eAAe,CAAC,KAAK,QAAQ;AACxD,SAAO,sBAAsB,OACzB,sBAAsB,KAAK,eAAe,QAC1C;GACJ,CACc,QAAQ,SAAS,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BjD,SAAS,YACP,UACgC;AAChC,QAAO,OAAO,OACZ,EAAE,EACF,GAAG,SAAS,KAAK,YAAY;AAI3B,SAAO,GAHY,iCACjB,QAAQ,mBAAmB,EAAE,CAC9B,GACqB,SAAQ;GAC9B,CACH;;AAOH,IAAM,iBAAiB;CACrB;CACA;CACA;CACD;AAED,IAAM,uBAAuB;CAC3B;CACA;CACA;CACD;AAED,SAAS,uBAAuB,KAAsB;AACpD,SAAQ,MACN,wCAAwC,IAAI,0EAC7C;AACD,QAAO;;AAGT,SAAgB,kBACd,SACA,WAAW,OACF;CACT,IAAI,aAAa;CACjB,MAAM,cAAc,OAAO,KAAK,QAAQ;AAGxC,EAAC,WACG,CAAC,GAAG,gBAAgB,GAAG,qBAAqB,GAC5C,gBACF,SAAS,QAAQ;AACjB,MAAI,CAAC,YAAY,SAAS,IAAI,CAC5B,cAAa,uBAAuB,IAAI;GAE1C;AAGF,KAAI,QAAQ,SAAS;EACnB,MAAM,cAAc,SAAS,QAAQ;AAErC,MAAI,YAAY,CAAC,aAAa,KAC5B,cAAa,uBAAuB,eAAe;AAIrD,MAAI,SAAS,QAAQ,IAAI,cAAc;GACrC,IAAI,oBAAoB,QAAQ,QAAQ,GAAG,aAAa;AAGxD,OAAI,YAAY,CAAC,mBAAmB,KAClC,cAAa,uBAAuB,4BAA4B;AAIlE,uBAAoB,QAAQ,QAAQ,GAAG,aAAa,QACjD,UAAU,CAAC,CAAC,OAAO,uBACrB,CAAC;AAGF,OAAI,mBAAmB,uBAErB,cAAa,yBACX,kBAAkB,wBAClB,+CACA,YACA,SACD;QAGH,cAAa,uBAAuB,uBAAuB;;AAK/D,KAAI,QAAQ,gCACV,cAAa,yBACX,QAAQ,iCACR,mCACA,YACA,SACD;AAIH,KAAI,CAAC,CAAC,QAAQ,oBAAoB,QAAQ,iBAAiB,GACzD,cAAa,yBACX,QAAQ,iBAAiB,IACzB,oBACA,YACA,SACD;AAGH,QAAQ,aAAa,UAAU,EAAE;;AAGnC,SAAS,yBACP,SACA,KACA,wBACA,UACS;CACT,IAAI,aAAa;AAEjB,KAAI,YAAY,CAAC,QAAQ,SAAS,OAChC,cAAa,uBAAuB,GAAG,IAAI,iBAAiB;AAE9D,KAAI,QAAQ,iBAAiB;EAC3B,MAAM,sBAAsB,QAAQ,gBAAgB;AACpD,MAAI,CAAC,qBAAqB,KACxB,cAAa,uBAAuB,GAAG,IAAI,uBAAuB;AAEpE,MAAI,CAAC,qBAAqB,MACxB,cAAa,uBAAuB,GAAG,IAAI,wBAAwB;OAGrE,cAAa,uBAAuB,GAAG,IAAI,kBAAkB;AAG/D,QAAO;;;;;;;AAQT,SAAgB,qCACd,SACkB;CAElB,MAAM,iBAAiB,kBAAkB,QAAQ;AAEjD,KAAI,CAAC,eAAe,QAAS,QAAO,EAAE;CAEtC,MAAM,oBAAoD,EAAE;AAC5D,gBAAe,QAAQ,KAAK,WAAW;AACrC,SAAO,cAAc,KAAK,UAAU;AAClC,OAAI,MAAM,wBAAwB;IAChC,MAAM,aAAa,yCACjB,MAAM,uBAAuB,gBAC9B;AACD,sBAAkB,cAAc,MAAM;;IAExC;GACF;AAEF,gBAAe,iBAAiB,KAAK,YAAY;EAC/C,MAAM,aAAa,yCACjB,QAAQ,gBACT;AACD,oBAAkB,cAAc;GAChC;CAEF,MAAM,kBAAkB,eAAe;AACvC,KAAI,iBAAiB;EACnB,MAAM,aAAa,yCACjB,gBAAgB,gBACjB;AACD,oBAAkB,cAAc;;AAGlC,QAAO,OAAO,OAAO,kBAAkB;;;;;;;AAQzC,SAAgB,kBACd,SACwB;CAExB,MAAM,iBAAiB,kBAAkB,SAAS,KAAK;AAEvD,KAAI,CAAC,eAAe,QAAS,QAAO,EAAE;CAEtC,MAAM,EACJ,SACA,iCAAiC,iBACjC,kBACA,yBACA,4BACA,QAAQ,kBACN;CAIJ,MAAM,qBAAqB,iBAAiB,gBAAgB,KACzD,WAAW,OAAO,KACpB;CACD,MAAM,kBAAkB,QAAQ,QAAQ,WAAW;AACjD,SAAO,sBAAsB,mBAAmB,QAAQ,OAAO,KAAK,IAAI;GACxE;CAGF,MAAM,wBAAwB,kBAAkB,QAAQ;CAGxD,MAAM,WAAW,YACf,kBAAkB,CAAC,iBAAiB,GAAG,iBAAiB,GAAG,iBAC5D;CAGD,MAAM,kBAAkB,iCACtB,kBAAkB,gBAAgB,kBAAkB,EAAE,CACvD;AA2DD,QAzDuB,gBAAgB,KAAK,QAAQ,gBAAgB;AAClE,SAAO;GACL,GAAG;GACH,cAAc,OAAO,aAAa,KAAK,UAAU;IAC/C,MAAM,qBAAqB,EAAC,GAAG,iBAAgB;AAG/C,uBAAmB,OAAO,QAAQ,MAAM;IAGxC,MAAM,YAAY,iCAChB,sBAAsB,EAAE,CACzB;IAOD,MAAM,aANc,sCAClB,sBAAsB,EAAE,EACxB,sBACD,CAG8B,MAAM,GAAG,cAAc,EAAE;IACxD,MAAM,SAAS,yCACb,YACA,2BAA2B,GAC5B;IACD,MAAM,YAAY,yCAChB,YACA,8BAA8B,GAC/B;IAGD,MAAM,UACJ,SAAS,cAAc,MAAM;IAG/B,IAAI,qBAAqB,EAAE;AAC3B,QAAI,QACF,sBAAqB,iCACnB,QAAQ,mBAAmB,EAAE,CAC9B;IAEH,MAAM,eAAe,IAAI,gBAAgB,mBAAmB;IAC5D,MAAM,SAAS,SAAS,SAAS,UAAU;AAE3C,WAAO;KACL,GAAG;KACH;KACA;KACA,iBAAiB,aAAa,UAAU;KACxC,UAAU,gBAAgB,OAAO,UAAU,MAAM;KACjD;KACA;KACA,oBAAoB,WAAW;KAChC;KACD;GACH;GACD"}