feat: Add support for exporting products in backend (#8214)

CLOSES CC-221
CLOSES CC-223
CLOSES CC-224
This commit is contained in:
Stevche Radevski
2024-07-22 13:40:04 +00:00
committed by GitHub
parent e9f1aafbb1
commit 0d2e7befbd
29 changed files with 615 additions and 690 deletions
@@ -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
1 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
2 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
3 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
4 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 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
2 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()