feat: define validators and use normalize-products step (#12473)
Fixes: FRMW-2965 In this PR we replace/remove the existing step to normalize a CSV file with the newly written CSV normalizer and also we validate the file contents further using a Zod schema. I have duplicated the schema for now. But it is makes sense to re-use the schema for CSV validating and `/admin/products/batch`, then I can keep one source of truth under utils and re-export it. WDYT? **Screenshots of some errors after validating the file strictly**  
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import { normalizeForExport } from "../helpers/normalize-for-export"
|
||||
import { convertJsonToCsv } from "../utlils"
|
||||
import { convertJsonToCsv } from "../utils"
|
||||
|
||||
const prodColumnPositions = new Map([
|
||||
["Product Id", 0],
|
||||
@@ -81,13 +81,13 @@ export const generateProductCsvStepId = "generate-product-csv"
|
||||
/**
|
||||
* This step generates a CSV file that exports products. The CSV
|
||||
* file is created and stored using the registered [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file).
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { data: products } = useQueryGraphStep({
|
||||
* entity: "product",
|
||||
* fields: ["*", "variants.*", "collection.*", "categories.*"]
|
||||
* })
|
||||
*
|
||||
*
|
||||
* // @ts-ignore
|
||||
* const data = generateProductCsvStep(products)
|
||||
*/
|
||||
@@ -118,7 +118,7 @@ export const generateProductCsvStep = createStep(
|
||||
})
|
||||
|
||||
return new StepResponse(
|
||||
{ id: file.id, filename } as GenerateProductCsvStepOutput,
|
||||
{ id: file.id, filename } as GenerateProductCsvStepOutput,
|
||||
file.id
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { HttpTypes, IProductModuleService } from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
* The products to group.
|
||||
*/
|
||||
export type GroupProductsForBatchStepInput = (HttpTypes.AdminCreateProduct & {
|
||||
/**
|
||||
* The ID of the product to update.
|
||||
*/
|
||||
id?: string
|
||||
})[]
|
||||
|
||||
export type GroupProductsForBatchStepOutput = {
|
||||
/**
|
||||
* The products to create.
|
||||
*/
|
||||
create: HttpTypes.AdminCreateProduct[]
|
||||
/**
|
||||
* The products to update.
|
||||
*/
|
||||
update: (HttpTypes.AdminUpdateProduct & { id: string })[]
|
||||
}
|
||||
|
||||
export const groupProductsForBatchStepId = "group-products-for-batch"
|
||||
/**
|
||||
* This step groups products to be created or updated.
|
||||
*
|
||||
* @example
|
||||
* const data = groupProductsForBatchStep([
|
||||
* {
|
||||
* id: "prod_123",
|
||||
* title: "Shirt",
|
||||
* options: [
|
||||
* {
|
||||
* title: "Size",
|
||||
* values: ["S", "M", "L"]
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* title: "Pants",
|
||||
* options: [
|
||||
* {
|
||||
* title: "Color",
|
||||
* values: ["Red", "Blue"]
|
||||
* }
|
||||
* ],
|
||||
* variants: [
|
||||
* {
|
||||
* title: "Red Pants",
|
||||
* options: {
|
||||
* Color: "Red"
|
||||
* },
|
||||
* prices: [
|
||||
* {
|
||||
* amount: 10,
|
||||
* currency_code: "usd"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ])
|
||||
*/
|
||||
export const groupProductsForBatchStep = createStep(
|
||||
groupProductsForBatchStepId,
|
||||
async (
|
||||
data: GroupProductsForBatchStepInput,
|
||||
{ container }
|
||||
) => {
|
||||
const service = container.resolve<IProductModuleService>(Modules.PRODUCT)
|
||||
|
||||
const existingProducts = await service.listProducts(
|
||||
{
|
||||
// We use the ID to do product updates
|
||||
id: data.map((product) => product.id).filter(Boolean) as string[],
|
||||
},
|
||||
{ select: ["handle"] }
|
||||
)
|
||||
const existingProductsSet = new Set(existingProducts.map((p) => p.id))
|
||||
|
||||
const { toUpdate, toCreate } = data.reduce(
|
||||
(
|
||||
acc: {
|
||||
toUpdate: (HttpTypes.AdminUpdateProduct & { id: string })[]
|
||||
toCreate: HttpTypes.AdminCreateProduct[]
|
||||
},
|
||||
product
|
||||
) => {
|
||||
// There are few data normalizations to do if we are dealing with an update.
|
||||
if (product.id && existingProductsSet.has(product.id)) {
|
||||
acc.toUpdate.push(
|
||||
product as HttpTypes.AdminUpdateProduct & { id: string }
|
||||
)
|
||||
return acc
|
||||
}
|
||||
|
||||
// New products and variants will be created with a new ID, even if there is one present in the CSV.
|
||||
// To add support for creating with predefined IDs we will need to do changes to the upsert method.
|
||||
delete product.id
|
||||
product.variants?.forEach((variant) => {
|
||||
delete (variant as any).id
|
||||
})
|
||||
|
||||
acc.toCreate.push(product)
|
||||
return acc
|
||||
},
|
||||
{ toUpdate: [], toCreate: [] }
|
||||
)
|
||||
|
||||
return new StepResponse(
|
||||
{ create: toCreate, update: toUpdate } as GroupProductsForBatchStepOutput,
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -23,7 +23,6 @@ export * from "./update-product-tags"
|
||||
export * from "./delete-product-tags"
|
||||
export * from "./generate-product-csv"
|
||||
export * from "./parse-product-csv"
|
||||
export * from "./group-products-for-batch"
|
||||
export * from "./wait-confirmation-product-import"
|
||||
export * from "./get-variant-availability"
|
||||
export * from "./normalize-products-v1"
|
||||
export * from "./normalize-products"
|
||||
|
||||
+16
-10
@@ -1,7 +1,7 @@
|
||||
import { CSVNormalizer } from "@medusajs/framework/utils"
|
||||
import { HttpTypes } from "@medusajs/framework/types"
|
||||
import { CSVNormalizer, productValidators } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import { convertCsvToJson } from "../utlils"
|
||||
import { GroupProductsForBatchStepOutput } from "./group-products-for-batch"
|
||||
import { convertCsvToJson } from "../utils"
|
||||
|
||||
/**
|
||||
* The CSV file content to parse.
|
||||
@@ -18,7 +18,7 @@ export const normalizeCsvStepId = "normalize-product-csv"
|
||||
*/
|
||||
export const normalizeCsvStep = createStep(
|
||||
normalizeCsvStepId,
|
||||
async (fileContent: NormalizeProductCsvStepInput, { container }) => {
|
||||
async (fileContent: NormalizeProductCsvStepInput) => {
|
||||
const csvProducts =
|
||||
convertCsvToJson<ConstructorParameters<typeof CSVNormalizer>[0][0]>(
|
||||
fileContent
|
||||
@@ -27,22 +27,28 @@ export const normalizeCsvStep = createStep(
|
||||
const products = normalizer.proccess()
|
||||
|
||||
const create = Object.keys(products.toCreate).reduce<
|
||||
(typeof products)["toCreate"][keyof (typeof products)["toCreate"]][]
|
||||
HttpTypes.AdminCreateProduct[]
|
||||
>((result, toCreateHandle) => {
|
||||
result.push(products.toCreate[toCreateHandle])
|
||||
result.push(
|
||||
productValidators.CreateProduct.parse(
|
||||
products.toCreate[toCreateHandle]
|
||||
) as HttpTypes.AdminCreateProduct
|
||||
)
|
||||
return result
|
||||
}, [])
|
||||
|
||||
const update = Object.keys(products.toUpdate).reduce<
|
||||
(typeof products)["toUpdate"][keyof (typeof products)["toUpdate"]][]
|
||||
>((result, toCreateId) => {
|
||||
result.push(products.toUpdate[toCreateId])
|
||||
HttpTypes.AdminUpdateProduct & { id: string }[]
|
||||
>((result, toUpdateId) => {
|
||||
result.push(
|
||||
productValidators.UpdateProduct.parse(products.toUpdate[toUpdateId])
|
||||
)
|
||||
return result
|
||||
}, [])
|
||||
|
||||
return new StepResponse({
|
||||
create,
|
||||
update,
|
||||
} as GroupProductsForBatchStepOutput)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -8,7 +8,7 @@ import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import { normalizeForImport } from "../helpers/normalize-for-import"
|
||||
import { normalizeV1Products } from "../helpers/normalize-v1-import"
|
||||
import { convertCsvToJson } from "../utlils"
|
||||
import { convertCsvToJson } from "../utils"
|
||||
|
||||
/**
|
||||
* The CSV file content to parse.
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
transform,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { notifyOnFailureStep, sendNotificationsStep } from "../../notification"
|
||||
import {
|
||||
groupProductsForBatchStep,
|
||||
parseProductCsvStep,
|
||||
waitConfirmationProductImportStep,
|
||||
} from "../steps"
|
||||
import { normalizeCsvStep, waitConfirmationProductImportStep } from "../steps"
|
||||
import { batchProductsWorkflow } from "./batch-products"
|
||||
|
||||
export const importProductsWorkflowId = "import-products"
|
||||
@@ -94,8 +90,7 @@ export const importProductsWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<WorkflowTypes.ProductWorkflow.ImportProductsDTO>
|
||||
): WorkflowResponse<WorkflowTypes.ProductWorkflow.ImportProductsSummary> => {
|
||||
const products = parseProductCsvStep(input.fileContent)
|
||||
const batchRequest = groupProductsForBatchStep(products)
|
||||
const batchRequest = normalizeCsvStep(input.fileContent)
|
||||
|
||||
const summary = transform({ batchRequest }, (data) => {
|
||||
return {
|
||||
|
||||
@@ -65,11 +65,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -78,7 +78,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
},
|
||||
"toUpdate": {},
|
||||
@@ -136,11 +136,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -157,11 +157,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -178,11 +178,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -191,7 +191,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
},
|
||||
"toUpdate": {},
|
||||
@@ -258,11 +258,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -280,11 +280,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -303,11 +303,11 @@ describe("CSV processor", () => {
|
||||
"origin_country": "EU",
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -316,7 +316,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
},
|
||||
"toUpdate": {},
|
||||
@@ -377,11 +377,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -398,11 +398,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -419,11 +419,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -440,11 +440,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -453,7 +453,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
"sweatpants": {
|
||||
"categories": [],
|
||||
@@ -498,11 +498,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -519,11 +519,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -540,11 +540,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -561,11 +561,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -574,7 +574,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
"t-shirt": {
|
||||
"categories": [],
|
||||
@@ -600,12 +600,8 @@ describe("CSV processor", () => {
|
||||
"title": "Size",
|
||||
"values": [
|
||||
"S",
|
||||
"S",
|
||||
"M",
|
||||
"M",
|
||||
"L",
|
||||
"L",
|
||||
"XL",
|
||||
"XL",
|
||||
],
|
||||
},
|
||||
@@ -614,12 +610,6 @@ describe("CSV processor", () => {
|
||||
"values": [
|
||||
"Black",
|
||||
"White",
|
||||
"Black",
|
||||
"White",
|
||||
"Black",
|
||||
"White",
|
||||
"Black",
|
||||
"White",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -643,11 +633,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -665,11 +655,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -687,11 +677,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -709,11 +699,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -731,11 +721,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -753,11 +743,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -775,11 +765,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -797,11 +787,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -810,7 +800,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
},
|
||||
"toUpdate": {
|
||||
@@ -858,11 +848,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -879,11 +869,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -900,11 +890,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -921,11 +911,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "10",
|
||||
"amount": 10,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"amount": 15,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -934,7 +924,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
"prod_01JT598HEWAE555V0A6BD602MG": {
|
||||
"categories": [],
|
||||
@@ -970,11 +960,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "1000",
|
||||
"amount": 1000,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "1200",
|
||||
"amount": 1200,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -982,7 +972,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
"prod_01JT598HEX26EHDG7SRK37Q3FG": {
|
||||
"categories": [],
|
||||
@@ -1024,11 +1014,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "2950",
|
||||
"amount": 2950,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "3350",
|
||||
"amount": 3350,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -1044,11 +1034,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "2950",
|
||||
"amount": 2950,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "3350",
|
||||
"amount": 3350,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -1064,11 +1054,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "2950",
|
||||
"amount": 2950,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "3350",
|
||||
"amount": 3350,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -1084,11 +1074,11 @@ describe("CSV processor", () => {
|
||||
},
|
||||
"prices": [
|
||||
{
|
||||
"amount": "2950",
|
||||
"amount": 2950,
|
||||
"currency_code": "eur",
|
||||
},
|
||||
{
|
||||
"amount": "3350",
|
||||
"amount": 3350,
|
||||
"currency_code": "usd",
|
||||
},
|
||||
],
|
||||
@@ -1096,7 +1086,7 @@ describe("CSV processor", () => {
|
||||
"variant_rank": 0,
|
||||
},
|
||||
],
|
||||
"weight": "400",
|
||||
"weight": 400,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,25 +169,17 @@ const productStaticColumns: {
|
||||
"product discountable",
|
||||
"discountable"
|
||||
),
|
||||
"product height": processAsNumber("product height", "height", {
|
||||
asNumericString: true,
|
||||
}),
|
||||
"product height": processAsNumber("product height", "height"),
|
||||
"product hs code": processAsString("product hs code", "hs_code"),
|
||||
"product length": processAsNumber("product length", "length", {
|
||||
asNumericString: true,
|
||||
}),
|
||||
"product length": processAsNumber("product length", "length"),
|
||||
"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 weight": processAsNumber("product weight", "weight"),
|
||||
"product width": processAsNumber("product width", "width"),
|
||||
"product metadata": processAsString("product metadata", "metadata"),
|
||||
"shipping profile id": processAsString(
|
||||
"shipping profile id",
|
||||
@@ -243,12 +235,8 @@ const variantStaticColumns: {
|
||||
"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 height": processAsNumber("variant height", "height"),
|
||||
"variant length": processAsNumber("variant length", "length"),
|
||||
"variant material": processAsString("variant material", "material"),
|
||||
"variant metadata": processAsString("variant metadata", "metadata"),
|
||||
"variant origin country": processAsString(
|
||||
@@ -259,12 +247,8 @@ const variantStaticColumns: {
|
||||
"variant variant rank",
|
||||
"variant_rank"
|
||||
),
|
||||
"variant width": processAsNumber("variant width", "width", {
|
||||
asNumericString: true,
|
||||
}),
|
||||
"variant weight": processAsNumber("variant weight", "weight", {
|
||||
asNumericString: true,
|
||||
}),
|
||||
"variant width": processAsNumber("variant width", "width"),
|
||||
"variant weight": processAsNumber("variant weight", "weight"),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +279,7 @@ const variantWildcardColumns: {
|
||||
} else {
|
||||
output["prices"].push({
|
||||
currency_code: iso,
|
||||
amount: String(numericValue),
|
||||
amount: numericValue,
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -438,7 +422,7 @@ export class CSVNormalizer {
|
||||
}, {})
|
||||
|
||||
if (unknownColumns.length) {
|
||||
return new MedusaError(
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid column name(s) "${unknownColumns.join('","')}"`
|
||||
)
|
||||
@@ -509,7 +493,7 @@ export class CSVNormalizer {
|
||||
)
|
||||
if (!matchingKey) {
|
||||
product.options.push({ title: key, values: [value] })
|
||||
} else {
|
||||
} else if (!matchingKey.values.includes(value)) {
|
||||
matchingKey.values.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum ProductStatus {
|
||||
DRAFT = "draft",
|
||||
PROPOSED = "proposed",
|
||||
PUBLISHED = "published",
|
||||
REJECTED = "rejected",
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
export enum ProductStatus {
|
||||
DRAFT = "draft",
|
||||
PROPOSED = "proposed",
|
||||
PUBLISHED = "published",
|
||||
REJECTED = "rejected",
|
||||
}
|
||||
|
||||
export * from "./events"
|
||||
export * from "./get-variant-availability"
|
||||
export * from "./enums"
|
||||
export * from "./csv-normalizer"
|
||||
export * from "./get-variant-availability"
|
||||
export * as productValidators from "./validators"
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { z } from "zod"
|
||||
import { ProductStatus } from "./enums"
|
||||
|
||||
export const booleanString = () =>
|
||||
z
|
||||
.union([z.boolean(), z.string()])
|
||||
.refine((value) => {
|
||||
return ["true", "false"].includes(value.toString().toLowerCase())
|
||||
})
|
||||
.transform((value) => {
|
||||
return value.toString().toLowerCase() === "true"
|
||||
})
|
||||
|
||||
export const numericString = () =>
|
||||
z.number().transform((value) => {
|
||||
return value !== null && value !== undefined ? String(value) : value
|
||||
})
|
||||
|
||||
export const IdAssociation = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
const statusEnum = z.nativeEnum(ProductStatus)
|
||||
|
||||
export const CreateVariantPrice = z.object({
|
||||
currency_code: z.string(),
|
||||
amount: z.number(),
|
||||
min_quantity: z.number().nullish(),
|
||||
max_quantity: z.number().nullish(),
|
||||
rules: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export const CreateProductOption = z.object({
|
||||
title: z.string(),
|
||||
values: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const CreateProductVariant = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
sku: z.string().nullish(),
|
||||
ean: z.string().nullish(),
|
||||
upc: z.string().nullish(),
|
||||
barcode: z.string().nullish(),
|
||||
hs_code: z.string().nullish(),
|
||||
mid_code: z.string().nullish(),
|
||||
allow_backorder: booleanString().optional().default(false),
|
||||
manage_inventory: booleanString().optional().default(true),
|
||||
variant_rank: z.number().optional(),
|
||||
weight: z.number().nullish(),
|
||||
length: z.number().nullish(),
|
||||
height: z.number().nullish(),
|
||||
width: z.number().nullish(),
|
||||
origin_country: z.string().nullish(),
|
||||
material: z.string().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
prices: z.array(CreateVariantPrice),
|
||||
options: z.record(z.string()).optional(),
|
||||
inventory_items: z
|
||||
.array(
|
||||
z.object({
|
||||
inventory_item_id: z.string(),
|
||||
required_quantity: z.number(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const CreateProduct = z
|
||||
.object({
|
||||
title: z.string(),
|
||||
subtitle: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
is_giftcard: booleanString().optional().default(false),
|
||||
discountable: booleanString().optional().default(true),
|
||||
images: z.array(z.object({ url: z.string() })).optional(),
|
||||
thumbnail: z.string().nullish(),
|
||||
handle: z.string().optional(),
|
||||
status: statusEnum.optional().default(ProductStatus.DRAFT),
|
||||
external_id: z.string().nullish(),
|
||||
type_id: z.string().nullish(),
|
||||
collection_id: z.string().nullish(),
|
||||
categories: z.array(IdAssociation).optional(),
|
||||
tags: z.array(IdAssociation).optional(),
|
||||
options: z.array(CreateProductOption).optional(),
|
||||
variants: z.array(CreateProductVariant).optional(),
|
||||
sales_channels: z.array(z.object({ id: z.string() })).optional(),
|
||||
shipping_profile_id: z.string().optional(),
|
||||
weight: z.number().nullish(),
|
||||
length: z.number().nullish(),
|
||||
height: z.number().nullish(),
|
||||
width: z.number().nullish(),
|
||||
hs_code: z.string().nullish(),
|
||||
mid_code: z.string().nullish(),
|
||||
origin_country: z.string().nullish(),
|
||||
material: z.string().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const UpdateProductOption = z.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
values: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const UpdateVariantPrice = z.object({
|
||||
id: z.string().optional(),
|
||||
currency_code: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
min_quantity: z.number().nullish(),
|
||||
max_quantity: z.number().nullish(),
|
||||
rules: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export const UpdateProductVariant = z
|
||||
.object({
|
||||
id: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
prices: z.array(UpdateVariantPrice).optional(),
|
||||
sku: z.string().nullish(),
|
||||
ean: z.string().nullish(),
|
||||
upc: z.string().nullish(),
|
||||
barcode: z.string().nullish(),
|
||||
hs_code: z.string().nullish(),
|
||||
mid_code: z.string().nullish(),
|
||||
allow_backorder: booleanString().optional(),
|
||||
manage_inventory: booleanString().optional(),
|
||||
variant_rank: z.number().optional(),
|
||||
weight: numericString().nullish(),
|
||||
length: numericString().nullish(),
|
||||
height: numericString().nullish(),
|
||||
width: numericString().nullish(),
|
||||
origin_country: z.string().nullish(),
|
||||
material: z.string().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
options: z.record(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const UpdateProduct = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
title: z.string().optional(),
|
||||
discountable: booleanString().optional(),
|
||||
is_giftcard: booleanString().optional(),
|
||||
options: z.array(UpdateProductOption).optional(),
|
||||
variants: z.array(UpdateProductVariant).optional(),
|
||||
status: statusEnum.optional(),
|
||||
subtitle: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
images: z.array(z.object({ url: z.string() })).optional(),
|
||||
thumbnail: z.string().nullish(),
|
||||
handle: z.string().nullish(),
|
||||
type_id: z.string().nullish(),
|
||||
external_id: z.string().nullish(),
|
||||
collection_id: z.string().nullish(),
|
||||
categories: z.array(IdAssociation).optional(),
|
||||
tags: z.array(IdAssociation).optional(),
|
||||
sales_channels: z.array(z.object({ id: z.string() })).optional(),
|
||||
shipping_profile_id: z.string().nullish(),
|
||||
weight: numericString().nullish(),
|
||||
length: numericString().nullish(),
|
||||
height: numericString().nullish(),
|
||||
width: numericString().nullish(),
|
||||
hs_code: z.string().nullish(),
|
||||
mid_code: z.string().nullish(),
|
||||
origin_country: z.string().nullish(),
|
||||
material: z.string().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
})
|
||||
.strict()
|
||||
Reference in New Issue
Block a user