fix(medusa): Export/import fixes including export fields that contains new line char (#2150)
This commit is contained in:
committed by
GitHub
parent
ee8fe3a88b
commit
b6161d2404
5
.changeset/small-walls-poke.md
Normal file
5
.changeset/small-walls-poke.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Handle new line char in csv cell and fix import strategy
|
||||||
@@ -173,6 +173,110 @@ describe("Batch job of product-export type", () => {
|
|||||||
expect(lineColumn[25]).toBe(productPayload.variants[0].sku)
|
expect(lineColumn[25]).toBe(productPayload.variants[0].sku)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should export a csv file containing the expected products including new line char in the cells", async () => {
|
||||||
|
jest.setTimeout(1000000)
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const productPayload = {
|
||||||
|
title: "Test export product",
|
||||||
|
description: "test-product-description\ntest line 2",
|
||||||
|
type: { value: "test-type" },
|
||||||
|
images: ["test-image.png", "test-image-2.png"],
|
||||||
|
collection_id: "test-collection",
|
||||||
|
tags: [{ value: "123" }, { value: "456" }],
|
||||||
|
options: [{ title: "size" }, { title: "color" }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
sku: "test-variant-sku-product-export",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "eur",
|
||||||
|
amount: 45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [{ value: "large" }, { value: "green" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const createProductRes = await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
productPayload,
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
const productId = createProductRes.data.product.id
|
||||||
|
const variantId = createProductRes.data.product.variants[0].id
|
||||||
|
|
||||||
|
const batchPayload = {
|
||||||
|
type: "product-export",
|
||||||
|
context: {
|
||||||
|
filterable_fields: {
|
||||||
|
title: "Test export product",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const batchJobRes = await api.post(
|
||||||
|
"/admin/batch-jobs",
|
||||||
|
batchPayload,
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
const batchJobId = batchJobRes.data.batch_job.id
|
||||||
|
|
||||||
|
expect(batchJobId).toBeTruthy()
|
||||||
|
|
||||||
|
// Pull to check the status until it is completed
|
||||||
|
let batchJob
|
||||||
|
let shouldContinuePulling = true
|
||||||
|
while (shouldContinuePulling) {
|
||||||
|
const res = await api.get(
|
||||||
|
`/admin/batch-jobs/${batchJobId}`,
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
await new Promise((resolve, _) => {
|
||||||
|
setTimeout(resolve, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
batchJob = res.data.batch_job
|
||||||
|
shouldContinuePulling = !(
|
||||||
|
batchJob.status === "completed" || batchJob.status === "failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(batchJob.status).toBe("completed")
|
||||||
|
|
||||||
|
exportFilePath = path.resolve(__dirname, batchJob.result.file_key)
|
||||||
|
const isFileExists = (await fs.stat(exportFilePath)).isFile()
|
||||||
|
|
||||||
|
expect(isFileExists).toBeTruthy()
|
||||||
|
|
||||||
|
const fileSize = (await fs.stat(exportFilePath)).size
|
||||||
|
expect(batchJob.result?.file_size).toBe(fileSize)
|
||||||
|
|
||||||
|
const data = (await fs.readFile(exportFilePath)).toString()
|
||||||
|
const [, ...lines] = data.split("\r\n").filter((l) => l)
|
||||||
|
|
||||||
|
expect(lines.length).toBe(1)
|
||||||
|
|
||||||
|
const lineColumn = lines[0].split(";")
|
||||||
|
|
||||||
|
expect(lineColumn[0]).toBe(productId)
|
||||||
|
expect(lineColumn[2]).toBe(productPayload.title)
|
||||||
|
expect(lineColumn[4]).toBe(`"${productPayload.description}"`)
|
||||||
|
expect(lineColumn[23]).toBe(variantId)
|
||||||
|
expect(lineColumn[24]).toBe(productPayload.variants[0].title)
|
||||||
|
expect(lineColumn[25]).toBe(productPayload.variants[0].sku)
|
||||||
|
})
|
||||||
|
|
||||||
it("should export a csv file containing a limited number of products", async () => {
|
it("should export a csv file containing a limited number of products", async () => {
|
||||||
jest.setTimeout(1000000)
|
jest.setTimeout(1000000)
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|||||||
@@ -111,145 +111,148 @@ describe("Product import batch job", () => {
|
|||||||
const productsResponse = await api.get("/admin/products", adminReqConfig)
|
const productsResponse = await api.get("/admin/products", adminReqConfig)
|
||||||
|
|
||||||
expect(productsResponse.data.count).toBe(2)
|
expect(productsResponse.data.count).toBe(2)
|
||||||
expect(productsResponse.data.products).toEqual([
|
expect(productsResponse.data.products).toEqual(
|
||||||
expect.objectContaining({
|
expect.arrayContaining([
|
||||||
id: "O6S1YQ6mKm",
|
expect.objectContaining({
|
||||||
title: "Test product",
|
id: "O6S1YQ6mKm",
|
||||||
description: "test-product-description-1",
|
title: "Test product",
|
||||||
handle: "test-product-product-1",
|
description:
|
||||||
is_giftcard: false,
|
"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",
|
||||||
status: "draft",
|
handle: "test-product-product-1",
|
||||||
thumbnail: "test-image.png",
|
is_giftcard: false,
|
||||||
variants: [
|
status: "draft",
|
||||||
expect.objectContaining({
|
thumbnail: "test-image.png",
|
||||||
title: "Test variant",
|
variants: [
|
||||||
product_id: "O6S1YQ6mKm",
|
expect.objectContaining({
|
||||||
sku: "test-sku-1",
|
title: "Test variant",
|
||||||
barcode: "test-barcode-1",
|
product_id: "O6S1YQ6mKm",
|
||||||
ean: null,
|
sku: "test-sku-1",
|
||||||
upc: null,
|
barcode: "test-barcode-1",
|
||||||
inventory_quantity: 10,
|
ean: null,
|
||||||
prices: [
|
upc: null,
|
||||||
expect.objectContaining({
|
inventory_quantity: 10,
|
||||||
currency_code: "eur",
|
prices: [
|
||||||
amount: 100,
|
expect.objectContaining({
|
||||||
region_id: "region-product-import-0",
|
currency_code: "eur",
|
||||||
}),
|
amount: 100,
|
||||||
expect.objectContaining({
|
region_id: "region-product-import-0",
|
||||||
currency_code: "usd",
|
}),
|
||||||
amount: 110,
|
expect.objectContaining({
|
||||||
}),
|
currency_code: "usd",
|
||||||
expect.objectContaining({
|
amount: 110,
|
||||||
currency_code: "dkk",
|
}),
|
||||||
amount: 130,
|
expect.objectContaining({
|
||||||
region_id: "region-product-import-1",
|
currency_code: "dkk",
|
||||||
}),
|
amount: 130,
|
||||||
],
|
region_id: "region-product-import-1",
|
||||||
options: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
value: "option 1 value red",
|
options: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
expect.objectContaining({
|
value: "option 1 value red",
|
||||||
value: "option 2 value 1",
|
}),
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
value: "option 2 value 1",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
images: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
url: "test-image.png",
|
images: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
url: "test-image.png",
|
||||||
options: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
title: "test-option-1",
|
options: [
|
||||||
product_id: "O6S1YQ6mKm",
|
expect.objectContaining({
|
||||||
}),
|
title: "test-option-1",
|
||||||
expect.objectContaining({
|
product_id: "O6S1YQ6mKm",
|
||||||
title: "test-option-2",
|
}),
|
||||||
product_id: "O6S1YQ6mKm",
|
expect.objectContaining({
|
||||||
}),
|
title: "test-option-2",
|
||||||
],
|
product_id: "O6S1YQ6mKm",
|
||||||
tags: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
value: "123_1",
|
tags: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
value: "123_1",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
id: "5VxiEkmnPV",
|
}),
|
||||||
title: "Test product",
|
expect.objectContaining({
|
||||||
description: "test-product-description",
|
id: "5VxiEkmnPV",
|
||||||
handle: "test-product-product-2",
|
title: "Test product",
|
||||||
is_giftcard: false,
|
description: "test-product-description",
|
||||||
status: "draft",
|
handle: "test-product-product-2",
|
||||||
thumbnail: "test-image.png",
|
is_giftcard: false,
|
||||||
profile_id: expect.any(String),
|
status: "draft",
|
||||||
variants: [
|
thumbnail: "test-image.png",
|
||||||
expect.objectContaining({
|
profile_id: expect.any(String),
|
||||||
title: "Test variant",
|
variants: [
|
||||||
product_id: "5VxiEkmnPV",
|
expect.objectContaining({
|
||||||
sku: "test-sku-2",
|
title: "Test variant",
|
||||||
barcode: "test-barcode-2",
|
product_id: "5VxiEkmnPV",
|
||||||
ean: null,
|
sku: "test-sku-2",
|
||||||
upc: null,
|
barcode: "test-barcode-2",
|
||||||
inventory_quantity: 10,
|
ean: null,
|
||||||
allow_backorder: false,
|
upc: null,
|
||||||
manage_inventory: true,
|
inventory_quantity: 10,
|
||||||
prices: [
|
allow_backorder: false,
|
||||||
expect.objectContaining({
|
manage_inventory: true,
|
||||||
currency_code: "dkk",
|
prices: [
|
||||||
amount: 110,
|
expect.objectContaining({
|
||||||
region_id: "region-product-import-2",
|
currency_code: "dkk",
|
||||||
}),
|
amount: 110,
|
||||||
],
|
region_id: "region-product-import-2",
|
||||||
options: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
value: "Option 1 value 1",
|
options: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
value: "Option 1 value 1",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
title: "Test variant",
|
}),
|
||||||
product_id: "5VxiEkmnPV",
|
expect.objectContaining({
|
||||||
sku: "test-sku-3",
|
title: "Test variant",
|
||||||
barcode: "test-barcode-3",
|
product_id: "5VxiEkmnPV",
|
||||||
ean: null,
|
sku: "test-sku-3",
|
||||||
upc: null,
|
barcode: "test-barcode-3",
|
||||||
inventory_quantity: 10,
|
ean: null,
|
||||||
allow_backorder: false,
|
upc: null,
|
||||||
manage_inventory: true,
|
inventory_quantity: 10,
|
||||||
prices: [
|
allow_backorder: false,
|
||||||
expect.objectContaining({
|
manage_inventory: true,
|
||||||
currency_code: "usd",
|
prices: [
|
||||||
amount: 120,
|
expect.objectContaining({
|
||||||
region_id: null,
|
currency_code: "usd",
|
||||||
}),
|
amount: 120,
|
||||||
],
|
region_id: null,
|
||||||
options: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
value: "Option 1 Value blue",
|
options: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
value: "Option 1 Value blue",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
images: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
url: "test-image.png",
|
images: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
url: "test-image.png",
|
||||||
options: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
title: "test-option",
|
options: [
|
||||||
product_id: "5VxiEkmnPV",
|
expect.objectContaining({
|
||||||
}),
|
title: "test-option",
|
||||||
],
|
product_id: "5VxiEkmnPV",
|
||||||
tags: [
|
}),
|
||||||
expect.objectContaining({
|
],
|
||||||
value: "123",
|
tags: [
|
||||||
}),
|
expect.objectContaining({
|
||||||
],
|
value: "123",
|
||||||
}),
|
}),
|
||||||
])
|
],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product Mid Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant Mid Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url,Sales Channel 1 Name,Sales Channel 2 Name,Sales Channel 1 Id,Sales Channel 2 Id
|
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url,Sales Channel 1 Name,Sales Channel 2 Name,Sales Channel 1 Id,Sales Channel 2 Id
|
||||||
O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,Import Sales Channel 1,Import Sales Channel 2,,
|
O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,Import Sales Channel 1,Import Sales Channel 2,,
|
||||||
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png,,,,
|
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png,,,,
|
||||||
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png,,,,
|
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png,,,,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product Mid Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant Mid Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url
|
Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url
|
||||||
O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png
|
O6S1YQ6mKm,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png
|
||||||
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png
|
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png
|
||||||
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png
|
5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { AbstractFileService } from "@medusajs/medusa"
|
import { AbstractFileService } from "@medusajs/medusa"
|
||||||
import stream from "stream"
|
import stream from "stream"
|
||||||
|
import { resolve } from "path"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import mkdirp from "mkdirp"
|
||||||
|
|
||||||
export default class LocalFileService extends AbstractFileService {
|
export default class LocalFileService extends AbstractFileService {
|
||||||
// eslint-disable-next-line no-empty-pattern
|
|
||||||
constructor({}, options) {
|
constructor({}, options) {
|
||||||
super({})
|
super({}, options)
|
||||||
this.upload_dir_ =
|
this.upload_dir_ =
|
||||||
process.env.UPLOAD_DIR ?? options.upload_dir ?? "uploads/images"
|
process.env.UPLOAD_DIR ?? options.upload_dir ?? "uploads/images"
|
||||||
|
|
||||||
@@ -15,49 +15,56 @@ export default class LocalFileService extends AbstractFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async upload(file) {
|
upload(file) {
|
||||||
const uploadPath = path.join(
|
return new Promise((resolvePromise, reject) => {
|
||||||
this.upload_dir_,
|
const path = resolve(this.upload_dir_, file.originalname)
|
||||||
path.dirname(file.originalname)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!fs.existsSync(uploadPath)) {
|
let content = ""
|
||||||
fs.mkdirSync(uploadPath, { recursive: true })
|
if (file.filename) {
|
||||||
}
|
content = fs.readFileSync(
|
||||||
|
resolve(process.cwd(), "uploads", file.filename)
|
||||||
const filePath = path.resolve(this.upload_dir_, file.originalname)
|
)
|
||||||
fs.writeFile(filePath, "", (error) => {
|
|
||||||
if (error) {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return { url: filePath }
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete({ name }) {
|
const pathSegments = path.split("/")
|
||||||
return new Promise((resolve, _) => {
|
pathSegments.splice(-1)
|
||||||
const path = resolve(this.upload_dir_, name)
|
const dirname = pathSegments.join("/")
|
||||||
fs.unlink(path, (err) => {
|
mkdirp.sync(dirname, { recursive: true })
|
||||||
|
|
||||||
|
fs.writeFile(path, content.toString(), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err
|
reject(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve("file unlinked")
|
resolvePromise({ url: path })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delete({ fileKey }) {
|
||||||
|
return new Promise((resolvePromise, reject) => {
|
||||||
|
const path = resolve(this.upload_dir_, fileKey)
|
||||||
|
fs.unlink(path, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePromise("file unlinked")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUploadStreamDescriptor({ name, ext }) {
|
async getUploadStreamDescriptor({ name, ext }) {
|
||||||
const fileKey = `${name}.${ext}`
|
const fileKey = `${name}.${ext}`
|
||||||
const filePath = path.resolve(this.upload_dir_, fileKey)
|
const path = resolve(this.upload_dir_, fileKey)
|
||||||
|
|
||||||
const isFileExists = fs.existsSync(filePath)
|
const isFileExists = fs.existsSync(path)
|
||||||
if (!isFileExists) {
|
if (!isFileExists) {
|
||||||
await this.upload({ originalname: fileKey })
|
await this.upload({ originalname: fileKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
const pass = new stream.PassThrough()
|
const pass = new stream.PassThrough()
|
||||||
pass.pipe(fs.createWriteStream(filePath))
|
pass.pipe(fs.createWriteStream(path))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
writeStream: pass,
|
writeStream: pass,
|
||||||
@@ -67,11 +74,23 @@ export default class LocalFileService extends AbstractFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDownloadStream(fileData) {
|
async getDownloadStream({ fileKey }) {
|
||||||
const filePath = path.resolve(
|
return new Promise((resolvePromise, reject) => {
|
||||||
this.upload_dir_,
|
try {
|
||||||
fileData.fileKey + (fileData.ext ? `.${fileData.ext}` : "")
|
const path = resolve(this.upload_dir_, fileKey)
|
||||||
)
|
const data = fs.readFileSync(path)
|
||||||
return fs.createReadStream(filePath)
|
const readable = stream.Readable()
|
||||||
|
readable._read = function () {}
|
||||||
|
readable.push(data.toString())
|
||||||
|
readable.push(null)
|
||||||
|
resolvePromise(readable)
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPresignedDownloadUrl({ fileKey }) {
|
||||||
|
return `${this.upload_dir_}/${fileKey}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,15 +101,14 @@ export default async (req, res) => {
|
|||||||
const validated = await validator(AdminPostBatchesReq, req.body)
|
const validated = await validator(AdminPostBatchesReq, req.body)
|
||||||
|
|
||||||
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
|
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
|
||||||
const toCreate = await batchJobService.prepareBatchJobForProcessing(
|
|
||||||
validated,
|
|
||||||
req
|
|
||||||
)
|
|
||||||
|
|
||||||
const userId = req.user.id ?? req.user.userId
|
const userId = req.user.id ?? req.user.userId
|
||||||
|
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
const batch_job = await manager.transaction(async (transactionManager) => {
|
const batch_job = await manager.transaction(async (transactionManager) => {
|
||||||
|
const toCreate = await batchJobService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.prepareBatchJobForProcessing(validated, req)
|
||||||
return await batchJobService.withTransaction(transactionManager).create({
|
return await batchJobService.withTransaction(transactionManager).create({
|
||||||
...toCreate,
|
...toCreate,
|
||||||
created_by: userId,
|
created_by: userId,
|
||||||
|
|||||||
@@ -372,11 +372,13 @@ class BatchJobService extends TransactionBaseService {
|
|||||||
data: CreateBatchJobInput,
|
data: CreateBatchJobInput,
|
||||||
req: Request
|
req: Request
|
||||||
): Promise<CreateBatchJobInput | never> {
|
): Promise<CreateBatchJobInput | never> {
|
||||||
return await this.atomicPhase_(async () => {
|
return await this.atomicPhase_(async (transactionManager) => {
|
||||||
const batchStrategy = this.strategyResolver_.resolveBatchJobByType(
|
const batchStrategy = this.strategyResolver_.resolveBatchJobByType(
|
||||||
data.type
|
data.type
|
||||||
)
|
)
|
||||||
return await batchStrategy.prepareBatchJobForProcessing(data, req)
|
return await batchStrategy
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.prepareBatchJobForProcessing(data, req)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ class ProductVariantService extends BaseService {
|
|||||||
const variantRes = await variantRepo.findOne({
|
const variantRes = await variantRepo.findOne({
|
||||||
where: { id: variantOrVariantId as string },
|
where: { id: variantOrVariantId as string },
|
||||||
})
|
})
|
||||||
if (typeof variant === "undefined") {
|
if (!isDefined(variantRes)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_FOUND,
|
MedusaError.Types.NOT_FOUND,
|
||||||
`Variant with id ${variantOrVariantId} was not found`
|
`Variant with id ${variantOrVariantId} was not found`
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ const variantIds = [
|
|||||||
export const productsToExport = [
|
export const productsToExport = [
|
||||||
{
|
{
|
||||||
sales_channels: [
|
sales_channels: [
|
||||||
{ id: IdMap.getId("sc_1"), name: "SC 1", description: "SC 1" },
|
{
|
||||||
|
id: IdMap.getId("sc_1"),
|
||||||
|
name: "SC 1",
|
||||||
|
description: "SC 1\nSC 1 second line\nSC 1 third line\nSC 1 forth line",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
collection: {
|
collection: {
|
||||||
created_at: "randomString",
|
created_at: "randomString",
|
||||||
@@ -26,7 +30,8 @@ export const productsToExport = [
|
|||||||
collection_id: IdMap.getId("product-export-collection_1"),
|
collection_id: IdMap.getId("product-export-collection_1"),
|
||||||
created_at: "randomString",
|
created_at: "randomString",
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
description: "test-product-description-1",
|
description:
|
||||||
|
"test-product-description-1\ntest-product-description-1 second line\ntest-product-description-1 third line\nforth line",
|
||||||
discountable: true,
|
discountable: true,
|
||||||
external_id: null,
|
external_id: null,
|
||||||
handle: "test-product-product-1",
|
handle: "test-product-product-1",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
exports[`Product export strategy should process the batch job and generate the appropriate output 1`] = `
|
exports[`Product export strategy should process the batch job and generate the appropriate output 1`] = `
|
||||||
Array [
|
Array [
|
||||||
"Product ID;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External ID;Product Profile Name;Product Profile Type;Variant ID;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow backorder;Variant Manage inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price france [USD];Price USD;Price denmark [DKK];Price Denmark [DKK];Option 1 Name;Option 1 Value;Option 2 Name;Option 2 Value;Image 1 Url
|
"Product id;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External ID;Product Profile Name;Product Profile Type;Variant id;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow backorder;Variant Manage inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price france [USD];Price USD;Price denmark [DKK];Price Denmark [DKK];Option 1 Name;Option 1 Value;Option 2 Name;Option 2 Value;Image 1 Url
|
||||||
",
|
",
|
||||||
"product-export-strategy-product-1;test-product-product-1;Test product;;test-product-description-1;draft;;;;;;;;;;Test collection 1;test-collection1;test-type-1;123_1;true;;profile_1;profile_type_1;product-export-strategy-variant-1;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;100;110;130;;test-option-1;option 1 value 1;test-option-2;option 2 value 1;test-image.png
|
"product-export-strategy-product-1;test-product-product-1;Test product;;\\"test-product-description-1\ntest-product-description-1 second line\ntest-product-description-1 third line\nforth line\\";draft;;;;;;;;;;Test collection 1;test-collection1;test-type-1;123_1;true;;profile_1;profile_type_1;product-export-strategy-variant-1;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;100;110;130;;test-option-1;option 1 value 1;test-option-2;option 2 value 1;test-image.png
|
||||||
",
|
",
|
||||||
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-2;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;;;110;test-option;Option 1 value 1;;;test-image.png
|
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-2;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;;;110;test-option;Option 1 value 1;;;test-image.png
|
||||||
",
|
",
|
||||||
@@ -15,9 +15,9 @@ Array [
|
|||||||
|
|
||||||
exports[`Product export strategy with sales channels should process the batch job and generate the appropriate output 1`] = `
|
exports[`Product export strategy with sales channels should process the batch job and generate the appropriate output 1`] = `
|
||||||
Array [
|
Array [
|
||||||
"Product ID;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External ID;Product Profile Name;Product Profile Type;Variant ID;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow backorder;Variant Manage inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price france [USD];Price USD;Price denmark [DKK];Price Denmark [DKK];Option 1 Name;Option 1 Value;Option 2 Name;Option 2 Value;Image 1 Url;Sales channel 1 Name;Sales channel 1 Description;Sales channel 2 Name;Sales channel 2 Description
|
"Product id;Product Handle;Product Title;Product Subtitle;Product Description;Product Status;Product Thumbnail;Product Weight;Product Length;Product Width;Product Height;Product HS Code;Product Origin Country;Product MID Code;Product Material;Product Collection Title;Product Collection Handle;Product Type;Product Tags;Product Discountable;Product External ID;Product Profile Name;Product Profile Type;Variant id;Variant Title;Variant SKU;Variant Barcode;Variant Inventory Quantity;Variant Allow backorder;Variant Manage inventory;Variant Weight;Variant Length;Variant Width;Variant Height;Variant HS Code;Variant Origin Country;Variant MID Code;Variant Material;Price france [USD];Price USD;Price denmark [DKK];Price Denmark [DKK];Option 1 Name;Option 1 Value;Option 2 Name;Option 2 Value;Image 1 Url;Sales channel 1 Name;Sales channel 1 Description;Sales channel 2 Name;Sales channel 2 Description
|
||||||
",
|
",
|
||||||
"product-export-strategy-product-1;test-product-product-1;Test product;;test-product-description-1;draft;;;;;;;;;;Test collection 1;test-collection1;test-type-1;123_1;true;;profile_1;profile_type_1;product-export-strategy-variant-1;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;100;110;130;;test-option-1;option 1 value 1;test-option-2;option 2 value 1;test-image.png;SC 1;SC 1;;
|
"product-export-strategy-product-1;test-product-product-1;Test product;;\\"test-product-description-1\ntest-product-description-1 second line\ntest-product-description-1 third line\nforth line\\";draft;;;;;;;;;;Test collection 1;test-collection1;test-type-1;123_1;true;;profile_1;profile_type_1;product-export-strategy-variant-1;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;100;110;130;;test-option-1;option 1 value 1;test-option-2;option 2 value 1;test-image.png;SC 1;\\"SC 1\nSC 1 second line\nSC 1 third line\nSC 1 forth line\\";;
|
||||||
",
|
",
|
||||||
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-2;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;;;110;test-option;Option 1 value 1;;;test-image.png;SC 1;SC 1;SC 2;SC 2
|
"product-export-strategy-product-2;test-product-product-2;Test product;;test-product-description;draft;;;;;;;;;;Test collection;test-collection2;test-type;123;true;;profile_2;profile_type_2;product-export-strategy-variant-2;Test variant;test-sku;test-barcode;10;false;true;;;;;;;;;;;;110;test-option;Option 1 value 1;;;test-image.png;SC 1;SC 1;SC 2;SC 2
|
||||||
",
|
",
|
||||||
|
|||||||
@@ -3,18 +3,24 @@ import { IdMap, MockManager } from "medusa-test-utils"
|
|||||||
import { User } from "../../../../models"
|
import { User } from "../../../../models"
|
||||||
import { BatchJobStatus } from "../../../../types/batch-job"
|
import { BatchJobStatus } from "../../../../types/batch-job"
|
||||||
import { productsToExport } from "../../../__fixtures__/product-export-data"
|
import { productsToExport } from "../../../__fixtures__/product-export-data"
|
||||||
import { AdminPostBatchesReq, defaultAdminProductRelations } from "../../../../api"
|
import {
|
||||||
|
AdminPostBatchesReq,
|
||||||
|
defaultAdminProductRelations,
|
||||||
|
} from "../../../../api"
|
||||||
import { ProductExportBatchJob } from "../../../batch-jobs/product"
|
import { ProductExportBatchJob } from "../../../batch-jobs/product"
|
||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
import { FlagRouter } from "../../../../utils/flag-router";
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels";
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||||
|
|
||||||
const productServiceMock = {
|
const productServiceMock = {
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
list: jest.fn().mockImplementation(() => Promise.resolve(productsToExport)),
|
list: jest.fn().mockImplementation(() => Promise.resolve(productsToExport)),
|
||||||
count: jest.fn().mockImplementation(() => Promise.resolve(productsToExport.length)),
|
|
||||||
|
count: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => Promise.resolve(productsToExport.length)),
|
||||||
listAndCount: jest.fn().mockImplementation(() => {
|
listAndCount: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve([productsToExport, productsToExport.length])
|
return Promise.resolve([productsToExport, productsToExport.length])
|
||||||
}),
|
}),
|
||||||
@@ -39,31 +45,31 @@ describe("Product export strategy", () => {
|
|||||||
write: (data: string) => {
|
write: (data: string) => {
|
||||||
outputDataStorage.push(data)
|
outputDataStorage.push(data)
|
||||||
},
|
},
|
||||||
end: () => void 0
|
end: () => void 0,
|
||||||
},
|
},
|
||||||
promise: Promise.resolve(),
|
promise: Promise.resolve(),
|
||||||
fileKey: 'product-export.csv'
|
fileKey: "product-export.csv",
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
let fakeJob = {
|
let fakeJob = {
|
||||||
id: IdMap.getId("product-export-job"),
|
id: IdMap.getId("product-export-job"),
|
||||||
type: 'product-export',
|
type: "product-export",
|
||||||
created_by: IdMap.getId("product-export-job-creator"),
|
created_by: IdMap.getId("product-export-job-creator"),
|
||||||
created_by_user: {} as User,
|
created_by_user: {} as User,
|
||||||
context: {},
|
context: {},
|
||||||
result: {},
|
result: {},
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
status: BatchJobStatus.PROCESSING as BatchJobStatus
|
status: BatchJobStatus.PROCESSING as BatchJobStatus,
|
||||||
} as ProductExportBatchJob
|
} as ProductExportBatchJob
|
||||||
|
|
||||||
let canceledFakeJob = {
|
let canceledFakeJob = {
|
||||||
...fakeJob,
|
...fakeJob,
|
||||||
id: "bj_failed",
|
id: "bj_failed",
|
||||||
status: BatchJobStatus.CANCELED
|
status: BatchJobStatus.CANCELED,
|
||||||
} as ProductExportBatchJob
|
} as ProductExportBatchJob
|
||||||
|
|
||||||
const batchJobServiceMock = {
|
const batchJobServiceMock = {
|
||||||
@@ -76,7 +82,7 @@ describe("Product export strategy", () => {
|
|||||||
...canceledFakeJob,
|
...canceledFakeJob,
|
||||||
...data,
|
...data,
|
||||||
context: { ...canceledFakeJob?.context, ...data?.context },
|
context: { ...canceledFakeJob?.context, ...data?.context },
|
||||||
result: { ...canceledFakeJob?.result, ...data?.result }
|
result: { ...canceledFakeJob?.result, ...data?.result },
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(canceledFakeJob)
|
return Promise.resolve(canceledFakeJob)
|
||||||
@@ -86,7 +92,7 @@ describe("Product export strategy", () => {
|
|||||||
...fakeJob,
|
...fakeJob,
|
||||||
...data,
|
...data,
|
||||||
context: { ...fakeJob?.context, ...data?.context },
|
context: { ...fakeJob?.context, ...data?.context },
|
||||||
result: { ...fakeJob?.result, ...data?.result }
|
result: { ...fakeJob?.result, ...data?.result },
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(fakeJob)
|
return Promise.resolve(fakeJob)
|
||||||
@@ -100,14 +106,12 @@ describe("Product export strategy", () => {
|
|||||||
return Promise.resolve(fakeJob)
|
return Promise.resolve(fakeJob)
|
||||||
}),
|
}),
|
||||||
retrieve: jest.fn().mockImplementation((id) => {
|
retrieve: jest.fn().mockImplementation((id) => {
|
||||||
const targetFakeJob = id === "bj_failed"
|
const targetFakeJob = id === "bj_failed" ? canceledFakeJob : fakeJob
|
||||||
? canceledFakeJob
|
|
||||||
: fakeJob
|
|
||||||
return Promise.resolve(targetFakeJob)
|
return Promise.resolve(targetFakeJob)
|
||||||
}),
|
}),
|
||||||
setFailed: jest.fn().mockImplementation((...args) => {
|
setFailed: jest.fn().mockImplementation((...args) => {
|
||||||
console.error(...args)
|
console.error(...args)
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const productExportStrategy = new ProductExportStrategy({
|
const productExportStrategy = new ProductExportStrategy({
|
||||||
@@ -118,11 +122,14 @@ describe("Product export strategy", () => {
|
|||||||
featureFlagRouter: new FlagRouter({}),
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should generate the appropriate template', async () => {
|
it("should generate the appropriate template", async () => {
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
fakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||||
const template = await productExportStrategy.buildHeader(fakeJob)
|
const template = await productExportStrategy.buildHeader(fakeJob)
|
||||||
expect(template).toMatch(/.*Product ID.*/)
|
expect(template).toMatch(/.*Product id.*/)
|
||||||
expect(template).toMatch(/.*Product Handle.*/)
|
expect(template).toMatch(/.*Product Handle.*/)
|
||||||
expect(template).toMatch(/.*Product Title.*/)
|
expect(template).toMatch(/.*Product Title.*/)
|
||||||
expect(template).toMatch(/.*Product Subtitle.*/)
|
expect(template).toMatch(/.*Product Subtitle.*/)
|
||||||
@@ -147,7 +154,7 @@ describe("Product export strategy", () => {
|
|||||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||||
|
|
||||||
expect(template).toMatch(/.*Variant ID.*/)
|
expect(template).toMatch(/.*Variant id.*/)
|
||||||
expect(template).toMatch(/.*Variant Title.*/)
|
expect(template).toMatch(/.*Variant Title.*/)
|
||||||
expect(template).toMatch(/.*Variant SKU.*/)
|
expect(template).toMatch(/.*Variant SKU.*/)
|
||||||
expect(template).toMatch(/.*Variant Barcode.*/)
|
expect(template).toMatch(/.*Variant Barcode.*/)
|
||||||
@@ -180,26 +187,29 @@ describe("Product export strategy", () => {
|
|||||||
expect(template).toMatch(/.*Image 1 Url.*/)
|
expect(template).toMatch(/.*Image 1 Url.*/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should process the batch job and generate the appropriate output', async () => {
|
it("should process the batch job and generate the appropriate output", async () => {
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
fakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||||
await productExportStrategy.processJob(fakeJob.id)
|
await productExportStrategy.processJob(fakeJob.id)
|
||||||
expect(outputDataStorage).toMatchSnapshot()
|
expect(outputDataStorage).toMatchSnapshot()
|
||||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should prepare the job to be pre proccessed', async () => {
|
it("should prepare the job to be pre processed", async () => {
|
||||||
const fakeJob1: AdminPostBatchesReq = {
|
const fakeJob1: AdminPostBatchesReq = {
|
||||||
type: 'product-export',
|
type: "product-export",
|
||||||
context: {
|
context: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
offset: 10,
|
offset: 10,
|
||||||
expand: "variants",
|
expand: "variants",
|
||||||
fields: "title",
|
fields: "title",
|
||||||
order: "-title",
|
order: "-title",
|
||||||
filterable_fields: { title: "test" }
|
filterable_fields: { title: "test" },
|
||||||
},
|
},
|
||||||
dry_run: false
|
dry_run: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const output1 = await productExportStrategy.prepareBatchJobForProcessing(
|
const output1 = await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
@@ -207,21 +217,23 @@ describe("Product export strategy", () => {
|
|||||||
{} as Express.Request
|
{} as Express.Request
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(output1.context).toEqual(expect.objectContaining({
|
expect(output1.context).toEqual(
|
||||||
list_config: {
|
expect.objectContaining({
|
||||||
select: ["title", "created_at", "id"],
|
list_config: {
|
||||||
order: { title: "DESC" },
|
select: ["title", "created_at", "id"],
|
||||||
relations: ["variants"],
|
order: { title: "DESC" },
|
||||||
skip: 10,
|
relations: ["variants"],
|
||||||
take: 10,
|
skip: 10,
|
||||||
},
|
take: 10,
|
||||||
filterable_fields: { title: "test" }
|
},
|
||||||
}))
|
filterable_fields: { title: "test" },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const fakeJob2: AdminPostBatchesReq = {
|
const fakeJob2: AdminPostBatchesReq = {
|
||||||
type: 'product-export',
|
type: "product-export",
|
||||||
context: {},
|
context: {},
|
||||||
dry_run: false
|
dry_run: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const output2 = await productExportStrategy.prepareBatchJobForProcessing(
|
const output2 = await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
@@ -229,19 +241,21 @@ describe("Product export strategy", () => {
|
|||||||
{} as Express.Request
|
{} as Express.Request
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(output2.context).toEqual(expect.objectContaining({
|
expect(output2.context).toEqual(
|
||||||
list_config: {
|
expect.objectContaining({
|
||||||
select: undefined,
|
list_config: {
|
||||||
order: { created_at: "DESC" },
|
select: undefined,
|
||||||
relations: [
|
order: { created_at: "DESC" },
|
||||||
...defaultAdminProductRelations,
|
relations: [
|
||||||
"variants.prices.region"
|
...defaultAdminProductRelations,
|
||||||
],
|
"variants.prices.region",
|
||||||
skip: 0,
|
],
|
||||||
take: 50,
|
skip: 0,
|
||||||
},
|
take: 50,
|
||||||
filterable_fields: undefined
|
},
|
||||||
}))
|
filterable_fields: undefined,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should always provide a file_key even with no data", async () => {
|
it("should always provide a file_key even with no data", async () => {
|
||||||
@@ -253,7 +267,10 @@ describe("Product export strategy", () => {
|
|||||||
featureFlagRouter: new FlagRouter({}),
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
fakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||||
await productExportStrategy.processJob(fakeJob.id)
|
await productExportStrategy.processJob(fakeJob.id)
|
||||||
|
|
||||||
@@ -269,7 +286,10 @@ describe("Product export strategy", () => {
|
|||||||
featureFlagRouter: new FlagRouter({}),
|
featureFlagRouter: new FlagRouter({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(canceledFakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
canceledFakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(canceledFakeJob.id)
|
await productExportStrategy.preProcessBatchJob(canceledFakeJob.id)
|
||||||
await productExportStrategy.processJob(canceledFakeJob.id)
|
await productExportStrategy.processJob(canceledFakeJob.id)
|
||||||
|
|
||||||
@@ -288,31 +308,31 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
write: (data: string) => {
|
write: (data: string) => {
|
||||||
outputDataStorage.push(data)
|
outputDataStorage.push(data)
|
||||||
},
|
},
|
||||||
end: () => void 0
|
end: () => void 0,
|
||||||
},
|
},
|
||||||
promise: Promise.resolve(),
|
promise: Promise.resolve(),
|
||||||
fileKey: 'product-export.csv'
|
fileKey: "product-export.csv",
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
let fakeJob = {
|
let fakeJob = {
|
||||||
id: IdMap.getId("product-export-job"),
|
id: IdMap.getId("product-export-job"),
|
||||||
type: 'product-export',
|
type: "product-export",
|
||||||
created_by: IdMap.getId("product-export-job-creator"),
|
created_by: IdMap.getId("product-export-job-creator"),
|
||||||
created_by_user: {} as User,
|
created_by_user: {} as User,
|
||||||
context: {},
|
context: {},
|
||||||
result: {},
|
result: {},
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
status: BatchJobStatus.PROCESSING as BatchJobStatus
|
status: BatchJobStatus.PROCESSING as BatchJobStatus,
|
||||||
} as ProductExportBatchJob
|
} as ProductExportBatchJob
|
||||||
|
|
||||||
let canceledFakeJob = {
|
let canceledFakeJob = {
|
||||||
...fakeJob,
|
...fakeJob,
|
||||||
id: "bj_failed",
|
id: "bj_failed",
|
||||||
status: BatchJobStatus.CANCELED
|
status: BatchJobStatus.CANCELED,
|
||||||
} as ProductExportBatchJob
|
} as ProductExportBatchJob
|
||||||
|
|
||||||
const batchJobServiceMock = {
|
const batchJobServiceMock = {
|
||||||
@@ -325,7 +345,7 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
...canceledFakeJob,
|
...canceledFakeJob,
|
||||||
...data,
|
...data,
|
||||||
context: { ...canceledFakeJob?.context, ...data?.context },
|
context: { ...canceledFakeJob?.context, ...data?.context },
|
||||||
result: { ...canceledFakeJob?.result, ...data?.result }
|
result: { ...canceledFakeJob?.result, ...data?.result },
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(canceledFakeJob)
|
return Promise.resolve(canceledFakeJob)
|
||||||
@@ -335,7 +355,7 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
...fakeJob,
|
...fakeJob,
|
||||||
...data,
|
...data,
|
||||||
context: { ...fakeJob?.context, ...data?.context },
|
context: { ...fakeJob?.context, ...data?.context },
|
||||||
result: { ...fakeJob?.result, ...data?.result }
|
result: { ...fakeJob?.result, ...data?.result },
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(fakeJob)
|
return Promise.resolve(fakeJob)
|
||||||
@@ -349,14 +369,12 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
return Promise.resolve(fakeJob)
|
return Promise.resolve(fakeJob)
|
||||||
}),
|
}),
|
||||||
retrieve: jest.fn().mockImplementation((id) => {
|
retrieve: jest.fn().mockImplementation((id) => {
|
||||||
const targetFakeJob = id === "bj_failed"
|
const targetFakeJob = id === "bj_failed" ? canceledFakeJob : fakeJob
|
||||||
? canceledFakeJob
|
|
||||||
: fakeJob
|
|
||||||
return Promise.resolve(targetFakeJob)
|
return Promise.resolve(targetFakeJob)
|
||||||
}),
|
}),
|
||||||
setFailed: jest.fn().mockImplementation((...args) => {
|
setFailed: jest.fn().mockImplementation((...args) => {
|
||||||
console.error(...args)
|
console.error(...args)
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const productExportStrategy = new ProductExportStrategy({
|
const productExportStrategy = new ProductExportStrategy({
|
||||||
@@ -369,11 +387,14 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should generate the appropriate template', async () => {
|
it("should generate the appropriate template", async () => {
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
fakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||||
const template = await productExportStrategy.buildHeader(fakeJob)
|
const template = await productExportStrategy.buildHeader(fakeJob)
|
||||||
expect(template).toMatch(/.*Product ID.*/)
|
expect(template).toMatch(/.*Product id.*/)
|
||||||
expect(template).toMatch(/.*Product Handle.*/)
|
expect(template).toMatch(/.*Product Handle.*/)
|
||||||
expect(template).toMatch(/.*Product Title.*/)
|
expect(template).toMatch(/.*Product Title.*/)
|
||||||
expect(template).toMatch(/.*Product Subtitle.*/)
|
expect(template).toMatch(/.*Product Subtitle.*/)
|
||||||
@@ -398,7 +419,7 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||||
expect(template).toMatch(/.*Product Profile Type.*/)
|
expect(template).toMatch(/.*Product Profile Type.*/)
|
||||||
|
|
||||||
expect(template).toMatch(/.*Variant ID.*/)
|
expect(template).toMatch(/.*Variant id.*/)
|
||||||
expect(template).toMatch(/.*Variant Title.*/)
|
expect(template).toMatch(/.*Variant Title.*/)
|
||||||
expect(template).toMatch(/.*Variant SKU.*/)
|
expect(template).toMatch(/.*Variant SKU.*/)
|
||||||
expect(template).toMatch(/.*Variant Barcode.*/)
|
expect(template).toMatch(/.*Variant Barcode.*/)
|
||||||
@@ -431,11 +452,14 @@ describe("Product export strategy with sales channels", () => {
|
|||||||
expect(template).toMatch(/.*Image 1 Url.*/)
|
expect(template).toMatch(/.*Image 1 Url.*/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should process the batch job and generate the appropriate output', async () => {
|
it("should process the batch job and generate the appropriate output", async () => {
|
||||||
await productExportStrategy.prepareBatchJobForProcessing(fakeJob, {} as Request)
|
await productExportStrategy.prepareBatchJobForProcessing(
|
||||||
|
fakeJob,
|
||||||
|
{} as Request
|
||||||
|
)
|
||||||
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
await productExportStrategy.preProcessBatchJob(fakeJob.id)
|
||||||
await productExportStrategy.processJob(fakeJob.id)
|
await productExportStrategy.processJob(fakeJob.id)
|
||||||
expect(outputDataStorage).toMatchSnapshot()
|
expect(outputDataStorage).toMatchSnapshot()
|
||||||
expect((fakeJob.result as any).file_key).toBeDefined()
|
expect((fakeJob.result as any).file_key).toBeDefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ let fakeJob = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function* generateCSVDataForStream() {
|
async function* generateCSVDataForStream() {
|
||||||
yield "Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product Mid Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant Mid Code,Variant Material,Price france [USD],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url\n"
|
yield "Product id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External ID,Product Profile Name,Product Profile Type,Variant id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow backorder,Variant Manage inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price france [USD],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url\n"
|
||||||
yield "O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,SebniWTDeC,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png\n"
|
yield "O6S1YQ6mKm,test-product-product-1,Test product,,test-product-description-1,draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,profile_1,profile_type_1,SebniWTDeC,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,100,110,130,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png\n"
|
||||||
yield "5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,CaBp7amx3r,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png\n"
|
yield "5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,CaBp7amx3r,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,110,test-option,Option 1 value 1,,,test-image.png\n"
|
||||||
yield "5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,3SS1MHGDEJ,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png\n"
|
yield "5VxiEkmnPV,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,profile_2,profile_type_2,3SS1MHGDEJ,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,120,,,test-option,Option 1 Value blue,,,test-image.png\n"
|
||||||
@@ -86,6 +86,9 @@ const productServiceMock = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shippingProfileServiceMock = {
|
const shippingProfileServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
retrieveDefault: jest.fn().mockImplementation((_data) => {
|
retrieveDefault: jest.fn().mockImplementation((_data) => {
|
||||||
return Promise.resolve({ id: "default_shipping_profile" })
|
return Promise.resolve({ id: "default_shipping_profile" })
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { FindProductConfig } from "../../../types/product"
|
import { FindProductConfig } from "../../../types/product"
|
||||||
import { FlagRouter } from "../../../utils/flag-router"
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||||
|
import { csvCellContentFormatter } from "../../../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -426,10 +427,16 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
|
|||||||
const variantLineData: string[] = []
|
const variantLineData: string[] = []
|
||||||
for (const [, columnSchema] of this.columnDescriptors.entries()) {
|
for (const [, columnSchema] of this.columnDescriptors.entries()) {
|
||||||
if (columnSchema.entityName === "product") {
|
if (columnSchema.entityName === "product") {
|
||||||
variantLineData.push(columnSchema.accessor(product))
|
const formattedContent = csvCellContentFormatter(
|
||||||
|
columnSchema.accessor(product)
|
||||||
|
)
|
||||||
|
variantLineData.push(formattedContent)
|
||||||
}
|
}
|
||||||
if (columnSchema.entityName === "variant") {
|
if (columnSchema.entityName === "variant") {
|
||||||
variantLineData.push(columnSchema.accessor(variant))
|
const formattedContent = csvCellContentFormatter(
|
||||||
|
columnSchema.accessor(variant)
|
||||||
|
)
|
||||||
|
variantLineData.push(formattedContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outputLineData.push(variantLineData.join(this.DELIMITER_) + this.NEWLINE_)
|
outputLineData.push(variantLineData.join(this.DELIMITER_) + this.NEWLINE_)
|
||||||
@@ -461,6 +468,12 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
|
|||||||
* The number of item of a relation can vary between 0-Infinity and therefore the number of columns
|
* The number of item of a relation can vary between 0-Infinity and therefore the number of columns
|
||||||
* that will be added to the export correspond to that number
|
* that will be added to the export correspond to that number
|
||||||
* @param products - The main entity to get the relation shape from
|
* @param products - The main entity to get the relation shape from
|
||||||
|
* @return ({
|
||||||
|
* optionColumnCount: number
|
||||||
|
* imageColumnCount: number
|
||||||
|
* salesChannelsColumnCount: number
|
||||||
|
* pricesData: Set<string>
|
||||||
|
* })
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private getProductRelationsDynamicColumnsShape(products: Product[]): {
|
private getProductRelationsDynamicColumnsShape(products: Product[]): {
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ import {
|
|||||||
ImportJobContext,
|
ImportJobContext,
|
||||||
InjectedProps,
|
InjectedProps,
|
||||||
OperationType,
|
OperationType,
|
||||||
|
ProductImportBatchJob,
|
||||||
ProductImportCsvSchema,
|
ProductImportCsvSchema,
|
||||||
TBuiltProductImportLine,
|
TBuiltProductImportLine,
|
||||||
TParsedProductImportRowData,
|
TParsedProductImportRowData,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { SalesChannel } from "../../../models"
|
import { BatchJob, SalesChannel } from "../../../models"
|
||||||
import { FlagRouter } from "../../../utils/flag-router"
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
import { transformProductData, transformVariantData } from "./utils"
|
import { transformProductData, transformVariantData } from "./utils"
|
||||||
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
|
||||||
@@ -135,7 +136,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
async getImportInstructions(
|
async getImportInstructions(
|
||||||
csvData: TParsedProductImportRowData[]
|
csvData: TParsedProductImportRowData[]
|
||||||
): Promise<Record<OperationType, TParsedProductImportRowData[]>> {
|
): Promise<Record<OperationType, TParsedProductImportRowData[]>> {
|
||||||
const shippingProfile = await this.shippingProfileService_.retrieveDefault()
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
const shippingProfile = await this.shippingProfileService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.retrieveDefault()
|
||||||
|
|
||||||
const seenProducts = {}
|
const seenProducts = {}
|
||||||
|
|
||||||
@@ -224,43 +228,64 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
* @param batchJobId - An id of a job that is being preprocessed.
|
* @param batchJobId - An id of a job that is being preprocessed.
|
||||||
*/
|
*/
|
||||||
async preProcessBatchJob(batchJobId: string): Promise<void> {
|
async preProcessBatchJob(batchJobId: string): Promise<void> {
|
||||||
const batchJob = await this.batchJobService_.retrieve(batchJobId)
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
const batchJob = await this.batchJobService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.retrieve(batchJobId)
|
||||||
|
|
||||||
const csvFileKey = (batchJob.context as ImportJobContext).fileKey
|
const csvFileKey = (batchJob.context as ImportJobContext).fileKey
|
||||||
const csvStream = await this.fileService_.getDownloadStream({
|
const csvStream = await this.fileService_.getDownloadStream({
|
||||||
fileKey: csvFileKey,
|
fileKey: csvFileKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
const parsedData = await this.csvParser_.parse(csvStream)
|
let builtData: Record<string, string>[]
|
||||||
const builtData = await this.csvParser_.buildData(parsedData)
|
try {
|
||||||
|
const parsedData = await this.csvParser_.parse(csvStream)
|
||||||
|
builtData = await this.csvParser_.buildData(parsedData)
|
||||||
|
} catch (e) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"The csv file parsing failed due to: " + e.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ops = await this.getImportInstructions(builtData)
|
const ops = await this.getImportInstructions(builtData)
|
||||||
|
|
||||||
await this.uploadImportOpsFile(batchJobId, ops)
|
await this.uploadImportOpsFile(batchJobId, ops)
|
||||||
|
|
||||||
await this.batchJobService_.update(batchJobId, {
|
let totalOperationCount = 0
|
||||||
result: {
|
const operationsCounts = {}
|
||||||
advancement_count: 0,
|
Object.keys(ops).forEach((key) => {
|
||||||
// number of update/create operations to execute
|
operationsCounts[key] = ops[key].length
|
||||||
count: Object.keys(ops).reduce((acc, k) => acc + ops[k].length, 0),
|
totalOperationCount += ops[key].length
|
||||||
stat_descriptors: [
|
})
|
||||||
{
|
|
||||||
key: "product-import-count",
|
await this.batchJobService_
|
||||||
name: "Products/variants to import",
|
.withTransaction(transactionManager)
|
||||||
message: `There will be ${
|
.update(batchJobId, {
|
||||||
ops[OperationType.ProductCreate].length
|
result: {
|
||||||
} products created (${
|
advancement_count: 0,
|
||||||
ops[OperationType.ProductUpdate].length
|
// number of update/create operations to execute
|
||||||
} updated).
|
count: totalOperationCount,
|
||||||
|
operations: operationsCounts,
|
||||||
|
stat_descriptors: [
|
||||||
|
{
|
||||||
|
key: "product-import-count",
|
||||||
|
name: "Products/variants to import",
|
||||||
|
message: `There will be ${
|
||||||
|
ops[OperationType.ProductCreate].length
|
||||||
|
} products created (${
|
||||||
|
ops[OperationType.ProductUpdate].length
|
||||||
|
} updated).
|
||||||
${
|
${
|
||||||
ops[OperationType.VariantCreate].length
|
ops[OperationType.VariantCreate].length
|
||||||
} variants will be created and ${
|
} variants will be created and ${
|
||||||
ops[OperationType.VariantUpdate].length
|
ops[OperationType.VariantUpdate].length
|
||||||
} updated`,
|
} updated`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -270,13 +295,17 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
* @param batchJobId - An id of a batch job that is being processed.
|
* @param batchJobId - An id of a batch job that is being processed.
|
||||||
*/
|
*/
|
||||||
async processJob(batchJobId: string): Promise<void> {
|
async processJob(batchJobId: string): Promise<void> {
|
||||||
return await this.atomicPhase_(async () => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
await this.createProducts(batchJobId)
|
const batchJob = (await this.batchJobService_
|
||||||
await this.updateProducts(batchJobId)
|
.withTransaction(manager)
|
||||||
await this.createVariants(batchJobId)
|
.retrieve(batchJobId)) as ProductImportBatchJob
|
||||||
await this.updateVariants(batchJobId)
|
|
||||||
|
|
||||||
this.finalize(batchJobId)
|
await this.createProducts(batchJob)
|
||||||
|
await this.updateProducts(batchJob)
|
||||||
|
await this.createVariants(batchJob)
|
||||||
|
await this.updateVariants(batchJob)
|
||||||
|
|
||||||
|
await this.finalize(batchJob)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,12 +355,17 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
/**
|
/**
|
||||||
* Method creates products using `ProductService` and parsed data from a CSV row.
|
* Method creates products using `ProductService` and parsed data from a CSV row.
|
||||||
*
|
*
|
||||||
* @param batchJobId - An id of the current batch job being processed.
|
* @param batchJob - The current batch job being processed.
|
||||||
*/
|
*/
|
||||||
private async createProducts(batchJobId: string): Promise<void> {
|
private async createProducts(batchJob: ProductImportBatchJob): Promise<void> {
|
||||||
|
if (!batchJob.result.operations[OperationType.ProductCreate]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
const productOps = await this.downloadImportOpsFile(
|
const productOps = await this.downloadImportOpsFile(
|
||||||
batchJobId,
|
batchJob.id,
|
||||||
OperationType.ProductCreate
|
OperationType.ProductCreate
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,19 +396,23 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateProgress(batchJobId)
|
await this.updateProgress(batchJob.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method updates existing products in the DB using a CSV row data.
|
* Method updates existing products in the DB using a CSV row data.
|
||||||
*
|
*
|
||||||
* @param batchJobId - An id of the current batch job being processed.
|
* @param batchJob - The current batch job being processed.
|
||||||
*/
|
*/
|
||||||
private async updateProducts(batchJobId: string): Promise<void> {
|
private async updateProducts(batchJob: ProductImportBatchJob): Promise<void> {
|
||||||
|
if (!batchJob.result.operations[OperationType.ProductUpdate]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
const productOps = await this.downloadImportOpsFile(
|
const productOps = await this.downloadImportOpsFile(
|
||||||
batchJobId,
|
batchJob.id,
|
||||||
OperationType.ProductUpdate
|
OperationType.ProductUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -405,7 +443,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(productOp, e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateProgress(batchJobId)
|
await this.updateProgress(batchJob.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,13 +451,17 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
* Method creates product variants from a CSV data.
|
* Method creates product variants from a CSV data.
|
||||||
* Method also handles processing of variant options.
|
* Method also handles processing of variant options.
|
||||||
*
|
*
|
||||||
* @param batchJobId - An id of the current batch job being processed.
|
* @param batchJob - The current batch job being processed.
|
||||||
*/
|
*/
|
||||||
private async createVariants(batchJobId: string): Promise<void> {
|
private async createVariants(batchJob: ProductImportBatchJob): Promise<void> {
|
||||||
|
if (!batchJob.result.operations[OperationType.VariantCreate]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
const variantOps = await this.downloadImportOpsFile(
|
const variantOps = await this.downloadImportOpsFile(
|
||||||
batchJobId,
|
batchJob.id,
|
||||||
OperationType.VariantCreate
|
OperationType.VariantCreate
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -452,7 +494,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.create(product!, variant as unknown as CreateProductVariantInput)
|
.create(product!, variant as unknown as CreateProductVariantInput)
|
||||||
|
|
||||||
this.updateProgress(batchJobId)
|
await this.updateProgress(batchJob.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
||||||
}
|
}
|
||||||
@@ -462,13 +504,17 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
/**
|
/**
|
||||||
* Method updates product variants from a CSV data.
|
* Method updates product variants from a CSV data.
|
||||||
*
|
*
|
||||||
* @param batchJobId - An id of the current batch job being processed.
|
* @param batchJob - The current batch job being processed.
|
||||||
*/
|
*/
|
||||||
private async updateVariants(batchJobId: string): Promise<void> {
|
private async updateVariants(batchJob: ProductImportBatchJob): Promise<void> {
|
||||||
|
if (!batchJob.result.operations[OperationType.VariantUpdate]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
const variantOps = await this.downloadImportOpsFile(
|
const variantOps = await this.downloadImportOpsFile(
|
||||||
batchJobId,
|
batchJob.id,
|
||||||
OperationType.VariantUpdate
|
OperationType.VariantUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -493,7 +539,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateProgress(batchJobId)
|
await this.updateProgress(batchJob.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,10 +553,13 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
variantOp,
|
variantOp,
|
||||||
productId: string
|
productId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
const productOptions = variantOp["variant.options"] || []
|
const productOptions = variantOp["variant.options"] || []
|
||||||
|
|
||||||
|
const productServiceTx =
|
||||||
|
this.productService_.withTransaction(transactionManager)
|
||||||
for (const o of productOptions) {
|
for (const o of productOptions) {
|
||||||
const option = await this.productService_.retrieveOptionByTitle(
|
const option = await productServiceTx.retrieveOptionByTitle(
|
||||||
o._title,
|
o._title,
|
||||||
productId
|
productId
|
||||||
)
|
)
|
||||||
@@ -536,7 +585,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
const { writeStream, promise } = await this.fileService_
|
const { writeStream, promise } = await this.fileService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.getUploadStreamDescriptor({
|
.getUploadStreamDescriptor({
|
||||||
name: `imports/products/ops/${batchJobId}-${op}`,
|
name: ProductImportStrategy.buildFilename(batchJobId, op),
|
||||||
ext: "json",
|
ext: "json",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -566,7 +615,9 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
const readableStream = await this.fileService_
|
const readableStream = await this.fileService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.getDownloadStream({
|
.getDownloadStream({
|
||||||
fileKey: `imports/products/ops/${batchJobId}-${op}.json`,
|
fileKey: ProductImportStrategy.buildFilename(batchJobId, op, {
|
||||||
|
appendExt: ".json",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return await new Promise((resolve) => {
|
return await new Promise((resolve) => {
|
||||||
@@ -591,10 +642,13 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
protected async deleteOpsFiles(batchJobId: string): Promise<void> {
|
protected async deleteOpsFiles(batchJobId: string): Promise<void> {
|
||||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
for (const op of Object.keys(OperationType)) {
|
const fileServiceTx = this.fileService_.withTransaction(transactionManager)
|
||||||
|
for (const op of Object.values(OperationType)) {
|
||||||
try {
|
try {
|
||||||
this.fileService_.withTransaction(transactionManager).delete({
|
await fileServiceTx.delete({
|
||||||
fileKey: `imports/products/ops/-${batchJobId}-${op}`,
|
fileKey: ProductImportStrategy.buildFilename(batchJobId, op, {
|
||||||
|
appendExt: ".json",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// noop
|
// noop
|
||||||
@@ -606,22 +660,26 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
* Update count of processed data in the batch job `result` column
|
* Update count of processed data in the batch job `result` column
|
||||||
* and cleanup temp JSON files.
|
* and cleanup temp JSON files.
|
||||||
*
|
*
|
||||||
* @param batchJobId - An id of the current batch job being processed.
|
* @param batchJob - The current batch job being processed.
|
||||||
*/
|
*/
|
||||||
private async finalize(batchJobId: string): Promise<void> {
|
private async finalize(batchJob: BatchJob): Promise<void> {
|
||||||
const batchJob = await this.batchJobService_.retrieve(batchJobId)
|
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
delete this.processedCounter[batchJobId]
|
delete this.processedCounter[batchJob.id]
|
||||||
|
|
||||||
await this.batchJobService_.update(batchJobId, {
|
await this.batchJobService_
|
||||||
result: { advancement_count: batchJob.result.count },
|
.withTransaction(transactionManager)
|
||||||
})
|
.update(batchJob.id, {
|
||||||
|
result: { advancement_count: batchJob.result.count },
|
||||||
|
})
|
||||||
|
|
||||||
const { fileKey } = batchJob.context as ImportJobContext
|
const { fileKey } = batchJob.context as ImportJobContext
|
||||||
|
|
||||||
await this.fileService_.delete({ fileKey })
|
await this.fileService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.delete({ fileKey })
|
||||||
|
|
||||||
await this.deleteOpsFiles(batchJobId)
|
await this.deleteOpsFiles(batchJob.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -639,11 +697,22 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.batchJobService_.update(batchJobId, {
|
await this.batchJobService_
|
||||||
result: {
|
.withTransaction(this.transactionManager_ ?? this.manager_)
|
||||||
advancement_count: newCount,
|
.update(batchJobId, {
|
||||||
},
|
result: {
|
||||||
})
|
advancement_count: newCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private static buildFilename(
|
||||||
|
batchJobId: string,
|
||||||
|
operation: string,
|
||||||
|
{ appendExt }: { appendExt?: string } = { appendExt: undefined }
|
||||||
|
): string {
|
||||||
|
const filename = `imports/products/ops/${batchJobId}-${operation}`
|
||||||
|
return appendExt ? filename + appendExt : filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,7 +744,7 @@ const CSVSchema: ProductImportCsvSchema = {
|
|||||||
{ name: "Product Height", mapTo: "product.height" },
|
{ name: "Product Height", mapTo: "product.height" },
|
||||||
{ name: "Product HS Code", mapTo: "product.hs_code" },
|
{ name: "Product HS Code", mapTo: "product.hs_code" },
|
||||||
{ name: "Product Origin Country", mapTo: "product.origin_country" },
|
{ name: "Product Origin Country", mapTo: "product.origin_country" },
|
||||||
{ name: "Product Mid Code", mapTo: "product.mid_code" },
|
{ name: "Product MID Code", mapTo: "product.mid_code" },
|
||||||
{ name: "Product Material", mapTo: "product.material" },
|
{ name: "Product Material", mapTo: "product.material" },
|
||||||
// PRODUCT-COLLECTION
|
// PRODUCT-COLLECTION
|
||||||
{ name: "Product Collection Title", mapTo: "product.collection.title" },
|
{ name: "Product Collection Title", mapTo: "product.collection.title" },
|
||||||
@@ -712,7 +781,7 @@ const CSVSchema: ProductImportCsvSchema = {
|
|||||||
{ name: "Variant Height", mapTo: "variant.height" },
|
{ name: "Variant Height", mapTo: "variant.height" },
|
||||||
{ name: "Variant HS Code", mapTo: "variant.hs_code" },
|
{ name: "Variant HS Code", mapTo: "variant.hs_code" },
|
||||||
{ name: "Variant Origin Country", mapTo: "variant.origin_country" },
|
{ name: "Variant Origin Country", mapTo: "variant.origin_country" },
|
||||||
{ name: "Variant Mid Code", mapTo: "variant.mid_code" },
|
{ name: "Variant MID Code", mapTo: "variant.mid_code" },
|
||||||
{ name: "Variant Material", mapTo: "variant.material" },
|
{ name: "Variant Material", mapTo: "variant.material" },
|
||||||
|
|
||||||
// ==== DYNAMIC FIELDS ====
|
// ==== DYNAMIC FIELDS ====
|
||||||
@@ -776,7 +845,6 @@ const CSVSchema: ProductImportCsvSchema = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const regionName = key.split(" ")[1]
|
const regionName = key.split(" ")[1]
|
||||||
|
|
||||||
;(
|
;(
|
||||||
builtLine["variant.prices"] as Record<string, string | number>[]
|
builtLine["variant.prices"] as Record<string, string | number>[]
|
||||||
).push({
|
).push({
|
||||||
@@ -802,7 +870,6 @@ const CSVSchema: ProductImportCsvSchema = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currency = key.split(" ")[1]
|
const currency = key.split(" ")[1]
|
||||||
|
|
||||||
;(
|
;(
|
||||||
builtLine["variant.prices"] as Record<string, string | number>[]
|
builtLine["variant.prices"] as Record<string, string | number>[]
|
||||||
).push({
|
).push({
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const productExportSchemaDescriptors = new Map<
|
|||||||
ProductExportColumnSchemaDescriptor
|
ProductExportColumnSchemaDescriptor
|
||||||
>([
|
>([
|
||||||
[
|
[
|
||||||
"Product ID",
|
"Product id",
|
||||||
{
|
{
|
||||||
accessor: (product: Product): string => product?.id ?? "",
|
accessor: (product: Product): string => product?.id ?? "",
|
||||||
entityName: "product",
|
entityName: "product",
|
||||||
@@ -219,7 +219,7 @@ export const productExportSchemaDescriptors = new Map<
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Variant ID",
|
"Variant id",
|
||||||
{
|
{
|
||||||
accessor: (variant: ProductVariant): string => variant?.id ?? "",
|
accessor: (variant: ProductVariant): string => variant?.id ?? "",
|
||||||
entityName: "variant",
|
entityName: "variant",
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ import {
|
|||||||
} from "../../../services"
|
} from "../../../services"
|
||||||
import { CsvSchema } from "../../../interfaces/csv-parser"
|
import { CsvSchema } from "../../../interfaces/csv-parser"
|
||||||
import { FlagRouter } from "../../../utils/flag-router"
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
|
import { BatchJob } from "../../../models"
|
||||||
|
|
||||||
|
export type ProductImportBatchJob = BatchJob & {
|
||||||
|
result: Pick<BatchJob, "result"> & {
|
||||||
|
operations: {
|
||||||
|
[K in keyof typeof OperationType]: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DI props for the Product import strategy
|
* DI props for the Product import strategy
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { TParsedProductImportRowData } from "./types"
|
||||||
|
import { csvRevertCellContentFormatter } from "../../../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pick keys for a new object by regex.
|
* Pick keys for a new object by regex.
|
||||||
* @param data - Initial data object
|
* @param data - Initial data object
|
||||||
* @param regex - A regex used to pick which keys are going to be copied in the new object
|
* @param regex - A regex used to pick which keys are going to be copied in the new object
|
||||||
*/
|
*/
|
||||||
import { TParsedProductImportRowData } from "./types"
|
|
||||||
|
|
||||||
export function pickObjectPropsByRegex(
|
export function pickObjectPropsByRegex(
|
||||||
data: TParsedProductImportRowData,
|
data: TParsedProductImportRowData,
|
||||||
regex: RegExp
|
regex: RegExp
|
||||||
@@ -14,7 +15,11 @@ export function pickObjectPropsByRegex(
|
|||||||
|
|
||||||
for (const k in data) {
|
for (const k in data) {
|
||||||
if (variantKeyPredicate(k)) {
|
if (variantKeyPredicate(k)) {
|
||||||
ret[k] = data[k]
|
const formattedData =
|
||||||
|
typeof data[k] === "string"
|
||||||
|
? csvRevertCellContentFormatter(data[k] as string)
|
||||||
|
: data[k]
|
||||||
|
ret[k] = formattedData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import BatchJobService from "../services/batch-job"
|
import BatchJobService from "../services/batch-job"
|
||||||
import EventBusService from "../services/event-bus"
|
import EventBusService from "../services/event-bus"
|
||||||
import { StrategyResolverService } from "../services"
|
import { StrategyResolverService } from "../services"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
batchJobService: BatchJobService
|
batchJobService: BatchJobService
|
||||||
strategyResolverService: StrategyResolverService
|
strategyResolverService: StrategyResolverService
|
||||||
|
manager: EntityManager
|
||||||
}
|
}
|
||||||
|
|
||||||
class BatchJobSubscriber {
|
class BatchJobSubscriber {
|
||||||
private readonly eventBusService_: EventBusService
|
private readonly eventBusService_: EventBusService
|
||||||
private readonly batchJobService_: BatchJobService
|
private readonly batchJobService_: BatchJobService
|
||||||
private readonly strategyResolver_: StrategyResolverService
|
private readonly strategyResolver_: StrategyResolverService
|
||||||
|
private readonly manager_: EntityManager
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
eventBusService,
|
eventBusService,
|
||||||
batchJobService,
|
batchJobService,
|
||||||
strategyResolverService,
|
strategyResolverService,
|
||||||
|
manager,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
this.eventBusService_ = eventBusService
|
this.eventBusService_ = eventBusService
|
||||||
this.batchJobService_ = batchJobService
|
this.batchJobService_ = batchJobService
|
||||||
this.strategyResolver_ = strategyResolverService
|
this.strategyResolver_ = strategyResolverService
|
||||||
|
this.manager_ = manager
|
||||||
|
|
||||||
this.eventBusService_
|
this.eventBusService_
|
||||||
.subscribe(BatchJobService.Events.CREATED, this.preProcessBatchJob)
|
.subscribe(BatchJobService.Events.CREATED, this.preProcessBatchJob)
|
||||||
@@ -28,37 +33,45 @@ class BatchJobSubscriber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
preProcessBatchJob = async (data): Promise<void> => {
|
preProcessBatchJob = async (data): Promise<void> => {
|
||||||
const batchJob = await this.batchJobService_.retrieve(data.id)
|
await this.manager_.transaction(async (manager) => {
|
||||||
|
const batchJobServiceTx = this.batchJobService_.withTransaction(manager)
|
||||||
|
const batchJob = await batchJobServiceTx.retrieve(data.id)
|
||||||
|
|
||||||
const batchJobStrategy = this.strategyResolver_.resolveBatchJobByType(
|
const batchJobStrategy = this.strategyResolver_.resolveBatchJobByType(
|
||||||
batchJob.type
|
batchJob.type
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await batchJobStrategy.preProcessBatchJob(batchJob.id)
|
await batchJobStrategy
|
||||||
await this.batchJobService_.setPreProcessingDone(batchJob.id)
|
.withTransaction(manager)
|
||||||
} catch (e) {
|
.preProcessBatchJob(batchJob.id)
|
||||||
await this.batchJobService_.setFailed(batchJob.id)
|
await batchJobServiceTx.setPreProcessingDone(batchJob.id)
|
||||||
throw e
|
} catch (e) {
|
||||||
}
|
await this.batchJobService_.setFailed(batchJob.id)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
processBatchJob = async (data): Promise<void> => {
|
processBatchJob = async (data): Promise<void> => {
|
||||||
const batchJob = await this.batchJobService_.retrieve(data.id)
|
await this.manager_.transaction(async (manager) => {
|
||||||
|
const batchJobServiceTx = this.batchJobService_.withTransaction(manager)
|
||||||
|
const batchJob = await batchJobServiceTx.retrieve(data.id)
|
||||||
|
|
||||||
const batchJobStrategy = this.strategyResolver_.resolveBatchJobByType(
|
const batchJobStrategy = this.strategyResolver_.resolveBatchJobByType(
|
||||||
batchJob.type
|
batchJob.type
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.batchJobService_.setProcessing(batchJob.id)
|
await batchJobServiceTx.setProcessing(batchJob.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await batchJobStrategy.processJob(batchJob.id)
|
await batchJobStrategy.withTransaction(manager).processJob(batchJob.id)
|
||||||
await this.batchJobService_.complete(batchJob.id)
|
await batchJobServiceTx.complete(batchJob.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.batchJobService_.setFailed(batchJob.id)
|
await this.batchJobService_.setFailed(batchJob.id)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { csvCellContentFormatter } from "../csv-cell-content-formatter"
|
||||||
|
|
||||||
|
type Case = {
|
||||||
|
str: string
|
||||||
|
expected: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases: [string, Case][] = [
|
||||||
|
[
|
||||||
|
"should return the exact input when there is no new line char",
|
||||||
|
{
|
||||||
|
str: "Hello, my name is Adrien and I like writing single line content.",
|
||||||
|
expected:
|
||||||
|
"Hello, my name is Adrien and I like writing single line content.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"should return a formatted string escaping new line when there is new line chars",
|
||||||
|
{
|
||||||
|
str: `Hello,
|
||||||
|
my name is Adrien and
|
||||||
|
I like writing multiline content
|
||||||
|
in a template string`,
|
||||||
|
expected:
|
||||||
|
'"Hello,\nmy name is Adrien and\nI like writing multiline content\nin a template string"',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"should return a formatted string escaping new line when there is new line chars and escape the double quote when there is double quotes",
|
||||||
|
{
|
||||||
|
str: 'Hello,\nmy name is "Adrien" and\nI like writing multiline content\nin a string',
|
||||||
|
expected:
|
||||||
|
'"Hello,\nmy name is ""Adrien"" and\nI like writing multiline content\nin a string"',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
describe("csvCellContentFormatter", function () {
|
||||||
|
it.each(cases)("%s", (title: string, { str, expected }: Case) => {
|
||||||
|
const formattedStr = csvCellContentFormatter(str)
|
||||||
|
expect(formattedStr).toBe(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
20
packages/medusa/src/utils/csv-cell-content-formatter.ts
Normal file
20
packages/medusa/src/utils/csv-cell-content-formatter.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function csvCellContentFormatter(str: string): string {
|
||||||
|
const newLineRegexp = new RegExp(/\n/g)
|
||||||
|
const doubleQuoteRegexp = new RegExp(/"/g)
|
||||||
|
|
||||||
|
const hasNewLineChar = !!str.match(newLineRegexp)
|
||||||
|
if (!hasNewLineChar) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatterStr = str.replace(doubleQuoteRegexp, '""')
|
||||||
|
|
||||||
|
return `"${formatterStr}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function csvRevertCellContentFormatter(str: string): string {
|
||||||
|
if (str.startsWith('"')) {
|
||||||
|
str = str.substring(1, str.length - 1)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from "./generate-entity-id"
|
|||||||
export * from "./remove-undefined-properties"
|
export * from "./remove-undefined-properties"
|
||||||
export * from "./is-defined"
|
export * from "./is-defined"
|
||||||
export * from "./calculate-price-tax-amount"
|
export * from "./calculate-price-tax-amount"
|
||||||
|
export * from "./csv-cell-content-formatter"
|
||||||
|
|||||||
Reference in New Issue
Block a user