feat: Add support for exporting products in backend (#8214)
CLOSES CC-221 CLOSES CC-223 CLOSES CC-224
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Product Tag 2,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value
|
||||
prod_01J3CRPNVGRZ01A8GH8FQYK10Z,Base product,,draft,,"test-product-description
|
||||
test line 2",base-product,false,true,test-image.png,pcol_01J3CRPNT6A0G5GG34MWHWE7QD,ptyp_01J3CRPNV39E51BGGWSKT674C5,,,,,,,,,2024-07-22T08:25:06.158Z,2024-07-22T08:25:06.158Z,,test-image.png,test-image-2.png,123,456,variant_01J3CRPNW5J6EBVVQP1TN33A58,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CRPNVGRZ01A8GH8FQYK10Z,2024-07-22T08:25:06.182Z,2024-07-22T08:25:06.182Z,,100,45,30,size,large,color,green
|
||||
prod_01J3CRPNVGRZ01A8GH8FQYK10Z,Base product,,draft,,"test-product-description
|
||||
test line 2",base-product,false,true,test-image.png,pcol_01J3CRPNT6A0G5GG34MWHWE7QD,ptyp_01J3CRPNV39E51BGGWSKT674C5,,,,,,,,,2024-07-22T08:25:06.158Z,2024-07-22T08:25:06.158Z,,test-image.png,test-image-2.png,123,456,variant_01J3CRPNW6NES6EN14X93F6YYB,Test variant 2,,,,,false,true,,,,,,,,,,0,prod_01J3CRPNVGRZ01A8GH8FQYK10Z,2024-07-22T08:25:06.182Z,2024-07-22T08:25:06.182Z,,200,65,50,size,small,color,green
|
||||
prod_01J3CRPNYJTCAV1QKRF6H0BY3M,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CRPNV39E51BGGWSKT674C5,,,,,,,,,2024-07-22T08:25:06.256Z,2024-07-22T08:25:06.256Z,,test-image.png,test-image-2.png,new-tag,,variant_01J3CRPNYZ6VZ5FVJ7WHJABV54,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CRPNYJTCAV1QKRF6H0BY3M,2024-07-22T08:25:06.271Z,2024-07-22T08:25:06.271Z,,100,45,30,size,large,color,green
|
||||
|
@@ -0,0 +1,2 @@
|
||||
Product Id,Product Title,Product Subtitle,Product Status,Product External Id,Product Description,Product Handle,Product Is Giftcard,Product Discountable,Product Thumbnail,Product Collection Id,Product Type Id,Product Weight,Product Length,Product Height,Product Width,Product Hs Code,Product Origin Country,Product Mid Code,Product Material,Product Created At,Product Updated At,Product Deleted At,Product Image 1,Product Image 2,Product Tag 1,Variant Id,Variant Title,Variant Sku,Variant Barcode,Variant Ean,Variant Upc,Variant Allow Backorder,Variant Manage Inventory,Variant Hs Code,Variant Origin Country,Variant Mid Code,Variant Material,Variant Weight,Variant Length,Variant Height,Variant Width,Variant Metadata,Variant Variant Rank,Variant Product Id,Variant Created At,Variant Updated At,Variant Deleted At,Variant Price USD,Variant Price EUR,Variant Price DKK,Variant Option 1 Name,Variant Option 1 Value,Variant Option 2 Name,Variant Option 2 Value
|
||||
prod_01J3CSN791SN1RN7X155Z8S9CN,Proposed product,,proposed,,test-product-description,proposed-product,false,true,test-image.png,,ptyp_01J3CSN76GCRSCDV9V489B5FWQ,,,,,,,,,2024-07-22T08:41:47.040Z,2024-07-22T08:41:47.040Z,,test-image.png,test-image-2.png,new-tag,variant_01J3CSN79CQ2ND94SRJSXMEMNH,Test variant,,,,,false,true,,,,,,,,,,0,prod_01J3CSN791SN1RN7X155Z8S9CN,2024-07-22T08:41:47.053Z,2024-07-22T08:41:47.053Z,,100,45,30,size,large,color,green
|
||||
|
@@ -1,12 +1,42 @@
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { IEventBusModuleService } from "@medusajs/types"
|
||||
import { TestEventUtils, medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const compareCSVs = async (filePath, expectedFilePath) => {
|
||||
let fileContent = await fs.readFile(filePath, { encoding: "utf-8" })
|
||||
let fixturesContent = await fs.readFile(expectedFilePath, {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
|
||||
// Normalize csv data to get rid of dynamic data
|
||||
const idsToReplace = ["prod_", "pcol_", "variant_", "ptyp_"]
|
||||
const dateRegex =
|
||||
/(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})\.(\d{3})Z/g
|
||||
idsToReplace.forEach((prefix) => {
|
||||
fileContent = fileContent.replace(
|
||||
new RegExp(`${prefix}\\w*\\d*`, "g"),
|
||||
"<ID>"
|
||||
)
|
||||
fixturesContent = fixturesContent.replace(
|
||||
new RegExp(`${prefix}\\w*\\d*`, "g"),
|
||||
"<ID>"
|
||||
)
|
||||
})
|
||||
fileContent = fileContent.replace(dateRegex, "<DATE>")
|
||||
fixturesContent = fixturesContent.replace(dateRegex, "<DATE>")
|
||||
|
||||
expect(fileContent).toEqual(fixturesContent)
|
||||
}
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let baseProduct
|
||||
@@ -17,6 +47,11 @@ medusaIntegrationTestRunner({
|
||||
|
||||
let baseType
|
||||
|
||||
let eventBus: IEventBusModuleService
|
||||
beforeAll(async () => {
|
||||
eventBus = getContainer().resolve(ModuleRegistrationName.EVENT_BUS)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||
|
||||
@@ -49,8 +84,53 @@ medusaIntegrationTestRunner({
|
||||
"/admin/products",
|
||||
getProductFixture({
|
||||
title: "Base product",
|
||||
description: "test-product-description\ntest line 2",
|
||||
collection_id: baseCollection.id,
|
||||
type_id: baseType.id,
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
},
|
||||
{
|
||||
currency_code: "eur",
|
||||
amount: 45,
|
||||
},
|
||||
{
|
||||
currency_code: "dkk",
|
||||
amount: 30,
|
||||
},
|
||||
],
|
||||
options: {
|
||||
size: "large",
|
||||
color: "green",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Test variant 2",
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
amount: 200,
|
||||
},
|
||||
{
|
||||
currency_code: "eur",
|
||||
amount: 65,
|
||||
},
|
||||
{
|
||||
currency_code: "dkk",
|
||||
amount: 50,
|
||||
},
|
||||
],
|
||||
options: {
|
||||
size: "small",
|
||||
color: "green",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
adminHeaders
|
||||
)
|
||||
@@ -72,6 +152,11 @@ medusaIntegrationTestRunner({
|
||||
|
||||
describe("POST /admin/products/export", () => {
|
||||
it("should export a csv file containing the expected products", async () => {
|
||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
||||
"notification.notification.created",
|
||||
eventBus
|
||||
)
|
||||
|
||||
// BREAKING: The batch endpoints moved to the domain routes (admin/batch-jobs -> /admin/products/export). The payload and response changed as well.
|
||||
const batchJobRes = await api.post(
|
||||
"/admin/products/export",
|
||||
@@ -82,205 +167,69 @@ medusaIntegrationTestRunner({
|
||||
const workflowId = batchJobRes.data.workflow_id
|
||||
expect(workflowId).toBeTruthy()
|
||||
|
||||
// Pull to check the status until it is completed
|
||||
while (true) {
|
||||
// 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"
|
||||
// )
|
||||
break
|
||||
}
|
||||
await subscriberExecution
|
||||
const notifications = (
|
||||
await api.get("/admin/notifications", adminHeaders)
|
||||
).data.notifications
|
||||
|
||||
// expect(batchJob.status).toBe("completed")
|
||||
expect(notifications.length).toBe(1)
|
||||
expect(notifications[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
title: "Product export",
|
||||
description: "Product export completed successfully!",
|
||||
file: expect.objectContaining({
|
||||
url: expect.stringContaining("-product-exports.csv"),
|
||||
filename: expect.any(String),
|
||||
mimeType: "text/csv",
|
||||
}),
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// 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)
|
||||
await compareCSVs(
|
||||
notifications[0].data.file.url,
|
||||
path.join(__dirname, "__fixtures__", "exported-products.csv")
|
||||
)
|
||||
await fs.rm(path.dirname(notifications[0].data.file.url), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
// it("should export a csv file containing the expected products including new line char in the cells", async () => {
|
||||
// const api = useApi()
|
||||
it("should export a csv file filtered by specific products", async () => {
|
||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
||||
"notification.notification.created",
|
||||
eventBus
|
||||
)
|
||||
|
||||
// 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
|
||||
// BREAKING: We don't support setting batch size in the export anymore
|
||||
const batchJobRes = await api.post(
|
||||
`/admin/products/export?id=${proposedProduct.id}`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// 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
|
||||
const workflowId = batchJobRes.data.workflow_id
|
||||
expect(workflowId).toBeTruthy()
|
||||
|
||||
// expect(batchJobId).toBeTruthy()
|
||||
await subscriberExecution
|
||||
const notifications = (
|
||||
await api.get("/admin/notifications", adminHeaders)
|
||||
).data.notifications
|
||||
|
||||
// // 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
|
||||
// )
|
||||
expect(notifications.length).toBe(1)
|
||||
|
||||
// await new Promise((resolve, _) => {
|
||||
// setTimeout(resolve, 1000)
|
||||
// })
|
||||
await compareCSVs(
|
||||
notifications[0].data.file.url,
|
||||
path.join(__dirname, "__fixtures__", "filtered-products.csv")
|
||||
)
|
||||
|
||||
// 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 () => {
|
||||
// const api = useApi()
|
||||
|
||||
// const batchPayload = {
|
||||
// type: "product-export",
|
||||
// context: {
|
||||
// batch_size: 1,
|
||||
// filterable_fields: { collection_id: "test-collection" },
|
||||
// order: "created_at",
|
||||
// },
|
||||
// }
|
||||
|
||||
// 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 data = (await fs.readFile(exportFilePath)).toString()
|
||||
// const [, ...lines] = data.split("\r\n").filter((l) => l)
|
||||
|
||||
// expect(lines.length).toBe(4)
|
||||
|
||||
// const csvLine = lines[0].split(";")
|
||||
// expect(csvLine[0]).toBe("test-product")
|
||||
// })
|
||||
await fs.rm(path.dirname(notifications[0].data.file.url), {
|
||||
force: true,
|
||||
recursive: true,
|
||||
})
|
||||
})
|
||||
|
||||
// it("should be able to import an exported csv file", async () => {
|
||||
// const api = useApi()
|
||||
|
||||
Reference in New Issue
Block a user