feat: create CSV normalizer to normalize a CSV file (#12396)
This commit is contained in:
1 parent
da270cd3e2
commit
4602163b56
13 files changed
+3305
-2
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: create CSV normalizer to normalize a CSV file
|
||||||
@@ -25,4 +25,5 @@ export * from "./generate-product-csv"
|
|||||||
export * from "./parse-product-csv"
|
export * from "./parse-product-csv"
|
||||||
export * from "./group-products-for-batch"
|
export * from "./group-products-for-batch"
|
||||||
export * from "./wait-confirmation-product-import"
|
export * from "./wait-confirmation-product-import"
|
||||||
export * from "./get-variant-availability"
|
export * from "./get-variant-availability"
|
||||||
|
export * from "./normalize-products-v1"
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { CSVNormalizer } from "@medusajs/framework/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { convertCsvToJson } from "../utlils"
|
||||||
|
import { GroupProductsForBatchStepOutput } from "./group-products-for-batch"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CSV file content to parse.
|
||||||
|
*/
|
||||||
|
export type NormalizeProductCsvStepInput = string
|
||||||
|
|
||||||
|
export const normalizeCsvStepId = "normalize-product-csv"
|
||||||
|
/**
|
||||||
|
* This step parses a CSV file holding products to import, returning the products as
|
||||||
|
* objects that can be imported.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const data = parseProductCsvStep("products.csv")
|
||||||
|
*/
|
||||||
|
export const normalizeCsvStep = createStep(
|
||||||
|
normalizeCsvStepId,
|
||||||
|
async (fileContent: NormalizeProductCsvStepInput, { container }) => {
|
||||||
|
const csvProducts =
|
||||||
|
convertCsvToJson<ConstructorParameters<typeof CSVNormalizer>[0][0]>(
|
||||||
|
fileContent
|
||||||
|
)
|
||||||
|
const normalizer = new CSVNormalizer(csvProducts)
|
||||||
|
const products = normalizer.proccess()
|
||||||
|
|
||||||
|
const create = Object.keys(products.toCreate).reduce<
|
||||||
|
(typeof products)["toCreate"][keyof (typeof products)["toCreate"]][]
|
||||||
|
>((result, toCreateHandle) => {
|
||||||
|
result.push(products.toCreate[toCreateHandle])
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const update = Object.keys(products.toUpdate).reduce<
|
||||||
|
(typeof products)["toUpdate"][keyof (typeof products)["toUpdate"]][]
|
||||||
|
>((result, toCreateId) => {
|
||||||
|
result.push(products.toUpdate[toCreateId])
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return new StepResponse({
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
} as GroupProductsForBatchStepOutput)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf dist && tsc --build",
|
"build": "rimraf dist && tsc --build",
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
"test": "jest --silent --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
|
"test": "jest --silent=false --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
|
||||||
"test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts"
|
"test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,3 +87,4 @@ export * from "./upper-case-first"
|
|||||||
export * from "./validate-handle"
|
export * from "./validate-handle"
|
||||||
export * from "./wrap-handler"
|
export * from "./wrap-handler"
|
||||||
export * from "./validate-module-name"
|
export * from "./validate-module-name"
|
||||||
|
export * from "./try-convert-to-boolean"
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Transforms a value to a boolean or returns the default value
|
||||||
|
* when original value cannot be casted to a boolean
|
||||||
|
*/
|
||||||
|
export function tryConvertToBoolean(value: unknown): boolean | undefined
|
||||||
|
export function tryConvertToBoolean<T>(
|
||||||
|
value: unknown,
|
||||||
|
defaultValue: T
|
||||||
|
): boolean | T
|
||||||
|
export function tryConvertToBoolean<T>(
|
||||||
|
value: unknown,
|
||||||
|
defaultValue?: T
|
||||||
|
): boolean | undefined | T {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const normalizedValue = value.toLowerCase()
|
||||||
|
return normalizedValue === "true"
|
||||||
|
? true
|
||||||
|
: normalizedValue === "false"
|
||||||
|
? false
|
||||||
|
: defaultValue ?? undefined
|
||||||
|
}
|
||||||
|
return defaultValue ?? undefined
|
||||||
|
}
|
||||||
+1236
File diff suppressed because it is too large.
Load diff
+149
@@ -0,0 +1,149 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY",
|
||||||
|
"Variant Title": "M",
|
||||||
|
"Variant Sku": "SWEATSHIRT-M",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "M",
|
||||||
|
"Variant Option 2 Name": "",
|
||||||
|
"Variant Option 2 Value": "",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3",
|
||||||
|
"Variant Title": "L",
|
||||||
|
"Variant Sku": "SWEATSHIRT-L",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "L",
|
||||||
|
"Variant Option 2 Name": "",
|
||||||
|
"Variant Option 2 Value": "",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT",
|
||||||
|
"Variant Title": "XL",
|
||||||
|
"Variant Sku": "SWEATSHIRT-XL",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "XL",
|
||||||
|
"Variant Option 2 Name": "",
|
||||||
|
"Variant Option 2 Value": "",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY",
|
||||||
|
"Variant Title": "M",
|
||||||
|
"Variant Sku": "SWEATSHIRT-M",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "M",
|
||||||
|
"Variant Option 2 Name": "",
|
||||||
|
"Variant Option 2 Value": "",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3",
|
||||||
|
"Variant Title": "BLACK",
|
||||||
|
"Variant Sku": "SWEATSHIRT-BLACK",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Color",
|
||||||
|
"Variant Option 1 Value": "Black",
|
||||||
|
"Variant Option 2 Name": "Size",
|
||||||
|
"Variant Option 2 Value": "L",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT",
|
||||||
|
"Variant Title": "XL",
|
||||||
|
"Variant Sku": "SWEATSHIRT-XL",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "XL",
|
||||||
|
"Variant Option 2 Name": "Color",
|
||||||
|
"Variant Option 2 Value": "White",
|
||||||
|
"Variant Origin Country": "EU",
|
||||||
|
"Variant Price EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Product Handle": "sweatshirt",
|
||||||
|
"Product Title": "Medusa Sweatshirt",
|
||||||
|
"Product Status": "published",
|
||||||
|
"Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
|
||||||
|
"Product Subtitle": "",
|
||||||
|
"Product External Id": "",
|
||||||
|
"Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Collection Id": "",
|
||||||
|
"Product Type Id": "",
|
||||||
|
"Product Discountable": true,
|
||||||
|
"Product Height": "",
|
||||||
|
"Product Hs Code": "",
|
||||||
|
"Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
|
||||||
|
"Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png",
|
||||||
|
"Product Image 3": "",
|
||||||
|
"Product Image 4": "",
|
||||||
|
"Product Length": "",
|
||||||
|
"Product Material": "",
|
||||||
|
"Product Mid Code": "",
|
||||||
|
"Product Origin Country": "",
|
||||||
|
"Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4",
|
||||||
|
"Product Weight": 400,
|
||||||
|
"Product Width": "",
|
||||||
|
"Variant Id": "variant_01JSXX3ZXFG4R7PVX55YQCZQPB",
|
||||||
|
"Variant Title": "S",
|
||||||
|
"Variant Sku": "SWEATSHIRT-S",
|
||||||
|
"Variant Upc": "",
|
||||||
|
"Variant Ean": "",
|
||||||
|
"Variant Hs Code": "",
|
||||||
|
"Variant Mid Code": "",
|
||||||
|
"Variant Manage Inventory": true,
|
||||||
|
"Variant Allow Backorder": false,
|
||||||
|
"Variant Barcode": "",
|
||||||
|
"Variant Height": "",
|
||||||
|
"Variant Length": "",
|
||||||
|
"Variant Material": "",
|
||||||
|
"Variant Metadata": "",
|
||||||
|
"Variant Option 1 Name": "Size",
|
||||||
|
"Variant Option 1 Value": "S",
|
||||||
|
"Variant Option 2 Name": "",
|
||||||
|
"Variant Option 2 Value": "",
|
||||||
|
"Variant Origin Country": "",
|
||||||
|
"Variant Price [Europe] EUR": 10,
|
||||||
|
"Variant Price USD": 15,
|
||||||
|
"Variant Variant Rank": 0,
|
||||||
|
"Variant Weight": "",
|
||||||
|
"Variant Width": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,533 @@
|
|||||||
|
import {
|
||||||
|
isPresent,
|
||||||
|
tryConvertToNumber,
|
||||||
|
tryConvertToBoolean,
|
||||||
|
MedusaError,
|
||||||
|
} from "../common"
|
||||||
|
import { AdminCreateProduct, AdminCreateProductVariant } from "@medusajs/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column processor is a function that process the CSV column
|
||||||
|
* and writes its value to the output
|
||||||
|
*/
|
||||||
|
type ColumnProcessor<Output> = (
|
||||||
|
csvRow: Record<string, string | boolean | number>,
|
||||||
|
rowColumns: string[],
|
||||||
|
rowNumber: number,
|
||||||
|
output: Output
|
||||||
|
) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an error with the CSV row number
|
||||||
|
*/
|
||||||
|
function createError(rowNumber: number, message: string) {
|
||||||
|
return new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Row ${rowNumber}: ${message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a CSV value by removing the leading "\r" from the
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
function normalizeValue<T>(value: T): T {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.replace(/\\r$/, "").trim() as T
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses different patterns to extract variant price iso
|
||||||
|
* and the region name. The iso is converted to lowercase
|
||||||
|
*/
|
||||||
|
function parseVariantPriceColumn(columnName: string, rowNumber: number) {
|
||||||
|
const normalizedValue = normalizeValue(columnName)
|
||||||
|
const potentialRegion = /\[(.*)\]/g.exec(normalizedValue)?.[1]
|
||||||
|
const iso = normalizedValue.split(" ").pop()
|
||||||
|
|
||||||
|
if (!iso) {
|
||||||
|
throw createError(
|
||||||
|
rowNumber,
|
||||||
|
`Invalid price format used by "${columnName}". Expect column name to contain the ISO code as the last segment. For example: "Variant Price [Europe] EUR" or "Variant Price EUR"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
iso: iso.toLowerCase(),
|
||||||
|
region: potentialRegion,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a column value as a string
|
||||||
|
*/
|
||||||
|
function processAsString<Output>(
|
||||||
|
inputKey: string,
|
||||||
|
outputKey: keyof Output
|
||||||
|
): ColumnProcessor<Output> {
|
||||||
|
return (csvRow, _, __, output) => {
|
||||||
|
const value = normalizeValue(csvRow[inputKey])
|
||||||
|
if (isPresent(value)) {
|
||||||
|
output[outputKey as any] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the column value as a boolean
|
||||||
|
*/
|
||||||
|
function processAsBoolean<Output>(
|
||||||
|
inputKey: string,
|
||||||
|
outputKey: keyof Output
|
||||||
|
): ColumnProcessor<Output> {
|
||||||
|
return (csvRow, _, __, output) => {
|
||||||
|
const value = normalizeValue(csvRow[inputKey])
|
||||||
|
if (isPresent(value)) {
|
||||||
|
output[outputKey as any] = tryConvertToBoolean(value, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the column value as a number
|
||||||
|
*/
|
||||||
|
function processAsNumber<Output>(
|
||||||
|
inputKey: string,
|
||||||
|
outputKey: keyof Output,
|
||||||
|
options?: { asNumericString: boolean }
|
||||||
|
): ColumnProcessor<Output> {
|
||||||
|
return (csvRow, _, rowNumber, output) => {
|
||||||
|
const value = normalizeValue(csvRow[inputKey])
|
||||||
|
if (isPresent(value)) {
|
||||||
|
const numericValue = tryConvertToNumber(value)
|
||||||
|
if (numericValue === undefined) {
|
||||||
|
throw createError(
|
||||||
|
rowNumber,
|
||||||
|
`Invalid value provided for "${inputKey}". Expected value to be a number, received "${value}"`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (options?.asNumericString) {
|
||||||
|
output[outputKey as any] = String(numericValue)
|
||||||
|
} else {
|
||||||
|
output[outputKey as any] = numericValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes the CSV column as a counter value. The counter values
|
||||||
|
* are defined as "<Column Name> <1>". Duplicate values are not
|
||||||
|
* added twice.
|
||||||
|
*/
|
||||||
|
function processAsCounterValue<Output extends Record<string, any[]>>(
|
||||||
|
inputMatcher: RegExp,
|
||||||
|
arrayItemKey: string,
|
||||||
|
outputKey: keyof Output
|
||||||
|
): ColumnProcessor<Output> {
|
||||||
|
return (csvRow, rowColumns, _, output) => {
|
||||||
|
output[outputKey] = output[outputKey] ?? []
|
||||||
|
const existingIds = output[outputKey].map((item) => item[arrayItemKey])
|
||||||
|
|
||||||
|
rowColumns
|
||||||
|
.filter((rowKey) => inputMatcher.test(rowKey))
|
||||||
|
.forEach((rowKey) => {
|
||||||
|
const value = normalizeValue(csvRow[rowKey])
|
||||||
|
if (!existingIds.includes(value) && isPresent(value)) {
|
||||||
|
output[outputKey].push({ [arrayItemKey]: value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of static product columns whose values must be copied
|
||||||
|
* as it is without any further processing.
|
||||||
|
*/
|
||||||
|
const productStaticColumns: {
|
||||||
|
[columnName: string]: ColumnProcessor<{
|
||||||
|
[K in keyof AdminCreateProduct | "id"]?: any
|
||||||
|
}>
|
||||||
|
} = {
|
||||||
|
"product id": processAsString("product id", "id"),
|
||||||
|
"product handle": processAsString("product handle", "handle"),
|
||||||
|
"product title": processAsString("product title", "title"),
|
||||||
|
"product status": processAsString("product status", "status"),
|
||||||
|
"product description": processAsString("product description", "description"),
|
||||||
|
"product subtitle": processAsString("product subtitle", "subtitle"),
|
||||||
|
"product external id": processAsString("product external id", "external_id"),
|
||||||
|
"product thumbnail": processAsString("product thumbnail", "thumbnail"),
|
||||||
|
"product collection id": processAsString(
|
||||||
|
"product collection id",
|
||||||
|
"collection_id"
|
||||||
|
),
|
||||||
|
"product type id": processAsString("product type id", "type_id"),
|
||||||
|
"product discountable": processAsBoolean(
|
||||||
|
"product discountable",
|
||||||
|
"discountable"
|
||||||
|
),
|
||||||
|
"product height": processAsNumber("product height", "height", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"product hs code": processAsString("product hs code", "hs_code"),
|
||||||
|
"product length": processAsNumber("product length", "length", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"product material": processAsString("product material", "material"),
|
||||||
|
"product mid code": processAsString("product mid code", "mid_code"),
|
||||||
|
"product origin country": processAsString(
|
||||||
|
"product origin country",
|
||||||
|
"origin_country"
|
||||||
|
),
|
||||||
|
"product weight": processAsNumber("product weight", "weight", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"product width": processAsNumber("product width", "width", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"product metadata": processAsString("product metadata", "metadata"),
|
||||||
|
"shipping profile id": processAsString(
|
||||||
|
"shipping profile id",
|
||||||
|
"shipping_profile_id"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of wildcard product columns whose values will be computed by
|
||||||
|
* one or more columns from the CSV row.
|
||||||
|
*/
|
||||||
|
const productWildcardColumns: {
|
||||||
|
[columnName: string]: ColumnProcessor<{
|
||||||
|
[K in keyof AdminCreateProduct]?: any
|
||||||
|
}>
|
||||||
|
} = {
|
||||||
|
"product category": processAsCounterValue(
|
||||||
|
/product category \d/,
|
||||||
|
"id",
|
||||||
|
"categories"
|
||||||
|
),
|
||||||
|
"product image": processAsCounterValue(/product image \d/, "url", "images"),
|
||||||
|
"product tag": processAsCounterValue(/product tag \d/, "id", "tags"),
|
||||||
|
"product sales channel": processAsCounterValue(
|
||||||
|
/product sales channel \d/,
|
||||||
|
"id",
|
||||||
|
"sales_channels"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of static variant columns whose values must be copied
|
||||||
|
* as it is without any further processing.
|
||||||
|
*/
|
||||||
|
const variantStaticColumns: {
|
||||||
|
[columnName: string]: ColumnProcessor<{
|
||||||
|
[K in keyof AdminCreateProductVariant | "id"]?: any
|
||||||
|
}>
|
||||||
|
} = {
|
||||||
|
"variant id": processAsString("variant id", "id"),
|
||||||
|
"variant title": processAsString("variant title", "title"),
|
||||||
|
"variant sku": processAsString("variant sku", "sku"),
|
||||||
|
"variant upc": processAsString("variant upc", "upc"),
|
||||||
|
"variant ean": processAsString("variant ean", "ean"),
|
||||||
|
"variant hs code": processAsString("variant hs code", "hs_code"),
|
||||||
|
"variant mid code": processAsString("variant mid code", "mid_code"),
|
||||||
|
"variant manage inventory": processAsBoolean(
|
||||||
|
"variant manage inventory",
|
||||||
|
"manage_inventory"
|
||||||
|
),
|
||||||
|
"variant allow backorder": processAsBoolean(
|
||||||
|
"variant allow backorder",
|
||||||
|
"allow_backorder"
|
||||||
|
),
|
||||||
|
"variant barcode": processAsString("variant barcode", "barcode"),
|
||||||
|
"variant height": processAsNumber("variant height", "height", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"variant length": processAsNumber("variant length", "length", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"variant material": processAsString("variant material", "material"),
|
||||||
|
"variant metadata": processAsString("variant metadata", "metadata"),
|
||||||
|
"variant origin country": processAsString(
|
||||||
|
"variant origin country",
|
||||||
|
"origin_country"
|
||||||
|
),
|
||||||
|
"variant variant rank": processAsString(
|
||||||
|
"variant variant rank",
|
||||||
|
"variant_rank"
|
||||||
|
),
|
||||||
|
"variant width": processAsNumber("variant width", "width", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
"variant weight": processAsNumber("variant weight", "weight", {
|
||||||
|
asNumericString: true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of wildcard variant columns whose values will be computed by
|
||||||
|
* one or more columns from the CSV row.
|
||||||
|
*/
|
||||||
|
const variantWildcardColumns: {
|
||||||
|
[columnName: string]: ColumnProcessor<{
|
||||||
|
[K in keyof AdminCreateProductVariant]?: any
|
||||||
|
}>
|
||||||
|
} = {
|
||||||
|
"variant price": (csvRow, rowColumns, rowNumber, output) => {
|
||||||
|
const pricesColumns = rowColumns.filter((rowKey) => {
|
||||||
|
return rowKey.startsWith("variant price ") && isPresent(csvRow[rowKey])
|
||||||
|
})
|
||||||
|
output["prices"] = output["prices"] ?? []
|
||||||
|
|
||||||
|
pricesColumns.forEach((columnName) => {
|
||||||
|
const { iso } = parseVariantPriceColumn(columnName, rowNumber)
|
||||||
|
const value = normalizeValue(csvRow[columnName])
|
||||||
|
|
||||||
|
const numericValue = tryConvertToNumber(value)
|
||||||
|
if (numericValue === undefined) {
|
||||||
|
throw createError(
|
||||||
|
rowNumber,
|
||||||
|
`Invalid value provided for "${columnName}". Expected value to be a number, received "${value}"`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
output["prices"].push({
|
||||||
|
currency_code: iso,
|
||||||
|
amount: String(numericValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options are processed separately and then defined on both the products and
|
||||||
|
* the variants.
|
||||||
|
*/
|
||||||
|
const optionColumns: {
|
||||||
|
[columnName: string]: ColumnProcessor<{
|
||||||
|
options: { key: any; value: any }[]
|
||||||
|
}>
|
||||||
|
} = {
|
||||||
|
"variant option": (csvRow, rowColumns, rowNumber, output) => {
|
||||||
|
const matcher = /variant option \d+ name/
|
||||||
|
const optionNameColumns = rowColumns.filter((rowKey) => {
|
||||||
|
return matcher.test(rowKey) && isPresent(normalizeValue(csvRow[rowKey]))
|
||||||
|
})
|
||||||
|
|
||||||
|
output["options"] = optionNameColumns.map((columnName) => {
|
||||||
|
const [, , counter] = columnName.split(" ")
|
||||||
|
const key = normalizeValue(csvRow[columnName])
|
||||||
|
const value = normalizeValue(csvRow[`variant option ${counter} value`])
|
||||||
|
|
||||||
|
if (!isPresent(value)) {
|
||||||
|
throw createError(rowNumber, `Missing option value for "${columnName}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of known columns
|
||||||
|
*/
|
||||||
|
const knownStaticColumns = Object.keys(productStaticColumns).concat(
|
||||||
|
Object.keys(variantStaticColumns)
|
||||||
|
)
|
||||||
|
const knownWildcardColumns = Object.keys(productWildcardColumns)
|
||||||
|
.concat(Object.keys(variantWildcardColumns))
|
||||||
|
.concat(Object.keys(optionColumns))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSV normalizer processes all the allowed columns from a CSV file and remaps
|
||||||
|
* them into a new object with properties matching the "AdminCreateProduct".
|
||||||
|
*
|
||||||
|
* However, further validations must be performed to validate the format and
|
||||||
|
* the required fields in the normalized output.
|
||||||
|
*/
|
||||||
|
export class CSVNormalizer {
|
||||||
|
#rows: Record<string, string | boolean | number>[]
|
||||||
|
|
||||||
|
#products: {
|
||||||
|
toCreate: {
|
||||||
|
[handle: string]: {
|
||||||
|
[K in keyof AdminCreateProduct]?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toUpdate: {
|
||||||
|
[id: string]: {
|
||||||
|
[K in keyof AdminCreateProduct]?: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
toCreate: {},
|
||||||
|
toUpdate: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(rows: Record<string, string | boolean | number>[]) {
|
||||||
|
this.#rows = rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures atleast one of the product id or the handle is provided. Otherwise
|
||||||
|
* we cannot process the row
|
||||||
|
*/
|
||||||
|
#ensureRowHasProductIdentifier(
|
||||||
|
row: Record<string, string | boolean | number>,
|
||||||
|
rowNumber: number
|
||||||
|
) {
|
||||||
|
const productId = row["product id"]
|
||||||
|
const productHandle = row["product handle"]
|
||||||
|
if (!isPresent(productId) && !isPresent(productHandle)) {
|
||||||
|
throw createError(
|
||||||
|
rowNumber,
|
||||||
|
"Missing product id and handle. One of them are required to process the row"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { productId, productHandle }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a product object or returns an existing one
|
||||||
|
* by its id. The products with ids are treated as updates
|
||||||
|
*/
|
||||||
|
#getOrInitializeProductById(id: string) {
|
||||||
|
if (!this.#products.toUpdate[id]) {
|
||||||
|
this.#products.toUpdate[id] = {}
|
||||||
|
}
|
||||||
|
return this.#products.toUpdate[id]!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a product object or returns an existing one
|
||||||
|
* by its handle. The products with handle are treated as creates
|
||||||
|
*/
|
||||||
|
#getOrInitializeProductByHandle(handle: string) {
|
||||||
|
if (!this.#products.toCreate[handle]) {
|
||||||
|
this.#products.toCreate[handle] = {}
|
||||||
|
}
|
||||||
|
return this.#products.toCreate[handle]!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a row by converting all keys to lowercase and creating a
|
||||||
|
* new object
|
||||||
|
*/
|
||||||
|
#normalizeRow(row: Record<string, any>) {
|
||||||
|
const unknownColumns: string[] = []
|
||||||
|
|
||||||
|
const normalized = Object.keys(row).reduce((result, key) => {
|
||||||
|
const lowerCaseKey = key.toLowerCase()
|
||||||
|
result[lowerCaseKey] = row[key]
|
||||||
|
|
||||||
|
if (
|
||||||
|
!knownStaticColumns.includes(lowerCaseKey) &&
|
||||||
|
!knownWildcardColumns.some((column) => lowerCaseKey.startsWith(column))
|
||||||
|
) {
|
||||||
|
unknownColumns.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
if (unknownColumns.length) {
|
||||||
|
return new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Invalid column name(s) "${unknownColumns.join('","')}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a given CSV row
|
||||||
|
*/
|
||||||
|
#processRow(
|
||||||
|
row: Record<string, string | boolean | number>,
|
||||||
|
rowNumber: number
|
||||||
|
) {
|
||||||
|
const rowColumns = Object.keys(row)
|
||||||
|
const { productHandle, productId } = this.#ensureRowHasProductIdentifier(
|
||||||
|
row,
|
||||||
|
rowNumber
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create representation of a product by its id or handle and process
|
||||||
|
* its static + wildcard columns
|
||||||
|
*/
|
||||||
|
const product = productId
|
||||||
|
? this.#getOrInitializeProductById(String(productId))
|
||||||
|
: this.#getOrInitializeProductByHandle(String(productHandle))
|
||||||
|
Object.keys(productStaticColumns).forEach((column) => {
|
||||||
|
productStaticColumns[column](row, rowColumns, rowNumber, product)
|
||||||
|
})
|
||||||
|
Object.keys(productWildcardColumns).forEach((column) => {
|
||||||
|
productWildcardColumns[column](row, rowColumns, rowNumber, product)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create representation of a variant and process
|
||||||
|
* its static + wildcard columns
|
||||||
|
*/
|
||||||
|
const variant: {
|
||||||
|
[K in keyof AdminCreateProductVariant]?: any
|
||||||
|
} = {}
|
||||||
|
Object.keys(variantStaticColumns).forEach((column) => {
|
||||||
|
variantStaticColumns[column](row, rowColumns, rowNumber, variant)
|
||||||
|
})
|
||||||
|
Object.keys(variantWildcardColumns).forEach((column) => {
|
||||||
|
variantWildcardColumns[column](row, rowColumns, rowNumber, variant)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process variant options as a standalone array
|
||||||
|
*/
|
||||||
|
const options: { options: { key: any; value: any }[] } = { options: [] }
|
||||||
|
Object.keys(optionColumns).forEach((column) => {
|
||||||
|
optionColumns[column](row, rowColumns, rowNumber, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify options on both the variant and the product
|
||||||
|
*/
|
||||||
|
options.options.forEach(({ key, value }) => {
|
||||||
|
variant.options = variant.options ?? {}
|
||||||
|
variant.options[key] = value
|
||||||
|
|
||||||
|
product.options = product.options ?? []
|
||||||
|
const matchingKey = product.options.find(
|
||||||
|
(option: any) => option.title === key
|
||||||
|
)
|
||||||
|
if (!matchingKey) {
|
||||||
|
product.options.push({ title: key, values: [value] })
|
||||||
|
} else {
|
||||||
|
matchingKey.values.push(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign variant to the product
|
||||||
|
*/
|
||||||
|
product.variants = product.variants ?? []
|
||||||
|
product.variants.push(variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process CSV rows. The return value is a tree of products
|
||||||
|
*/
|
||||||
|
proccess() {
|
||||||
|
this.#rows.forEach((row, index) =>
|
||||||
|
this.#processRow(this.#normalizeRow(row), index + 1)
|
||||||
|
)
|
||||||
|
return this.#products
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ export enum ProductStatus {
|
|||||||
|
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
export * from "./get-variant-availability"
|
export * from "./get-variant-availability"
|
||||||
|
export * from "./csv-normalizer"
|
||||||
Reference in new issue
Block a user