Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa): Align product import and export #2471

Merged
merged 14 commits into from
Oct 20, 2022
136 changes: 124 additions & 12 deletions integration-tests/api/__tests__/batch-jobs/product/export.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const path = require("path")
const fs = require("fs/promises")
import { sep, resolve } from "path"
import { resolve, sep } from "path"

const setupServer = require("../../../../helpers/setup-server")
const { useApi } = require("../../../../helpers/use-api")
Expand All @@ -16,7 +16,7 @@ const adminReqConfig = {
},
}

jest.setTimeout(1000000)
jest.setTimeout(100000000)

describe("Batch job of product-export type", () => {
let medusaProcess
Expand Down Expand Up @@ -56,21 +56,26 @@ describe("Batch job of product-export type", () => {
const db = useDb()
await db.teardown()

const isFileExists = (await fs.stat(exportFilePath))?.isFile()
try {
const isFileExists = (await fs.stat(exportFilePath))?.isFile()

if (isFileExists) {
const [, relativeRoot] = exportFilePath.replace(__dirname, "").split(sep)
if (isFileExists) {
const [, relativeRoot] = exportFilePath
.replace(__dirname, "")
.split(sep)

if ((await fs.stat(resolve(__dirname, relativeRoot)))?.isDirectory()) {
topDir = relativeRoot
}
if ((await fs.stat(resolve(__dirname, relativeRoot)))?.isDirectory()) {
topDir = relativeRoot
}

await fs.unlink(exportFilePath)
await fs.unlink(exportFilePath)
}
} catch (e) {
console.log(e)
}
})

it("should export a csv file containing the expected products", async () => {
jest.setTimeout(1000000)
const api = useApi()

const productPayload = {
Expand Down Expand Up @@ -174,7 +179,6 @@ describe("Batch job of product-export type", () => {
})

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 = {
Expand Down Expand Up @@ -278,7 +282,6 @@ describe("Batch job of product-export type", () => {
})

it("should export a csv file containing a limited number of products", async () => {
jest.setTimeout(1000000)
const api = useApi()

const batchPayload = {
Expand Down Expand Up @@ -333,4 +336,113 @@ describe("Batch job of product-export type", () => {
const csvLine = lines[0].split(";")
expect(csvLine[0]).toBe("test-product")
})

it("should be able to import an exported csv file", 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
)
let 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 [header, ...lines] = data.split("\r\n").filter((l) => l)

expect(lines.length).toBe(4)

const csvLine = lines[0].split(";")
expect(csvLine[0]).toBe("test-product")
expect(csvLine[2]).toBe("Test product")

csvLine[2] = "Updated test product"
lines.splice(0, 1, csvLine.join(";"))

await fs.writeFile(exportFilePath, [header, ...lines].join("\r\n"))

const importBatchJobRes = await api.post(
"/admin/batch-jobs",
{
type: "product-import",
context: {
fileKey: exportFilePath,
},
},
adminReqConfig
)

batchJobId = importBatchJobRes.data.batch_job.id

expect(batchJobId).toBeTruthy()

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")

const productsResponse = await api.get("/admin/products", adminReqConfig)
expect(productsResponse.data.count).toBe(5)
expect(productsResponse.data.products).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: csvLine[0],
handle: csvLine[1],
title: csvLine[2],
}),
])
)
})
})
63 changes: 61 additions & 2 deletions integration-tests/api/__tests__/batch-jobs/product/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("Product import batch job", () => {
await db.teardown()
})

it("should import a csv file", async () => {
it.only("should import a csv file", async () => {
adrien2p marked this conversation as resolved.
Show resolved Hide resolved
jest.setTimeout(1000000)
const api = useApi()

Expand Down Expand Up @@ -133,7 +133,7 @@ describe("Product import batch job", () => {
expect(batchJob.status).toBe("completed")

const productsResponse = await api.get("/admin/products", adminReqConfig)
expect(productsResponse.data.count).toBe(2)
expect(productsResponse.data.count).toBe(3)
expect(productsResponse.data.products).toEqual(
expect.arrayContaining([
// NEW PRODUCT
Expand Down Expand Up @@ -200,6 +200,65 @@ describe("Product import batch job", () => {
}),
],
}),
expect.objectContaining({
title: "Test product",
description:
"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",
handle: "test-product-product-1-1",
is_giftcard: false,
status: "draft",
thumbnail: "test-image.png",
variants: [
// NEW VARIANT
expect.objectContaining({
title: "Test variant",
sku: "test-sku-1-1",
barcode: "test-barcode-1-1",
ean: null,
upc: null,
inventory_quantity: 10,
prices: [
expect.objectContaining({
currency_code: "eur",
amount: 100,
region_id: "region-product-import-0",
}),
expect.objectContaining({
currency_code: "usd",
amount: 110,
}),
expect.objectContaining({
currency_code: "dkk",
amount: 130,
region_id: "region-product-import-1",
}),
],
options: expect.arrayContaining([
expect.objectContaining({
value: "option 1 value red",
}),
expect.objectContaining({
value: "option 2 value 1",
}),
]),
}),
],
type: null,
images: [
expect.objectContaining({
url: "test-image.png",
}),
],
options: [
expect.objectContaining({
title: "test-option-1",
}),
expect.objectContaining({
title: "test-option-2",
}),
],
tags: [],
}),
// UPDATED PRODUCT
expect.objectContaining({
id: existingProductToBeUpdated.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
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,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,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
,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,,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,1.30,,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,,

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"jest": "jest",
"test": "turbo run test",
"test:integration": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js",
"test:integration:api": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/api",
"test:integration:api": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/api -- integration-tests/api/__tests__/batch-jobs/product/import.js",
adrien2p marked this conversation as resolved.
Show resolved Hide resolved
"test:integration:plugins": "NODE_ENV=test jest --runInBand --bail --config=integration-tests/jest.config.js --projects=integration-tests/plugins",
"test:fixtures": "NODE_ENV=test jest --config=docs-util/jest.config.js --runInBand --bail",
"openapi:generate": "node ./scripts/build-openapi.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/medusa/src/interfaces/batch-job-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TransactionBaseService } from "./transaction-base-service"
import { BatchJobResultError, CreateBatchJobInput } from "../types/batch-job"
import { ProductExportBatchJob } from "../strategies/batch-jobs/product"
import { ProductExportBatchJob } from "../strategies/batch-jobs/product/types"
import { BatchJobService } from "../services"
import { BatchJob } from "../models"

Expand Down
33 changes: 21 additions & 12 deletions packages/medusa/src/interfaces/csv-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,30 @@ export abstract class AbstractCsvValidator<TCsvLine, TBuiltLine>
): Promise<boolean | never>
}

export type CsvSchemaColumn<TCsvLine, TBuiltLine> = {
name: string
export type CsvSchemaColumn<
TCsvLine,
TBuiltLine,
NameAsOptional = false
> = (NameAsOptional extends false
? {
name: string
}
: {
name?: string
}) & {
required?: boolean
validator?: AbstractCsvValidator<TCsvLine, TBuiltLine>
} & (
| {
mapTo?: string
transform?: ColumnTransformer<TCsvLine>
}
| {
match?: RegExp
reducer?: ColumnReducer<TCsvLine, TBuiltLine>
transform?: ColumnTransformer<TCsvLine>
}
)
| {
mapTo?: string
transform?: ColumnTransformer<TCsvLine>
}
| {
match?: RegExp
reducer?: ColumnReducer<TCsvLine, TBuiltLine>
transform?: ColumnTransformer<TCsvLine>
}
)

export type ColumnTransformer<TCsvLine> = (
value: string,
Expand Down
3 changes: 2 additions & 1 deletion packages/medusa/src/loaders/strategies.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import glob from "glob"
import path from "path"
import { asFunction, aliasTo } from "awilix"
import { aliasTo, asFunction } from "awilix"

import formatRegistrationName from "../utils/format-registration-name"
import { isBatchJobStrategy } from "../interfaces"
Expand Down Expand Up @@ -36,6 +36,7 @@ export default ({ container, configModule, isTest }: LoaderOptions): void => {
"**/utils.ts",
"**/types.js",
"**/types.ts",
"**/types/**",
],
})

Expand Down
Loading