feat(orchestration,core-flows,medusa,product,types,utils): product import/export uses workflows (#5811)

This commit is contained in:
Riqwan Thamir
2023-12-12 13:09:25 +01:00
committed by GitHub
parent 6f96ced40f
commit 07107f3565
30 changed files with 1601 additions and 357 deletions

View File

@@ -0,0 +1,471 @@
import fs from "fs/promises"
import path, { resolve, sep } from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { simpleSalesChannelFactory } from "../../../../factories"
import adminSeeder from "../../../../helpers/admin-seeder"
import productSeeder from "../../../../helpers/product-seeder"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
const setupServer = require("../../../../environment-helpers/setup-server")
const userSeeder = require("../../../../helpers/user-seeder")
const adminReqConfig = {
headers: {
"x-medusa-access-token": "test_token",
},
}
const env: Record<any, any> = {
MEDUSA_FF_MEDUSA_V2: true,
}
jest.setTimeout(180000)
describe("Batch job of product-export type", () => {
let medusaProcess
let dbConnection
let exportFilePath = ""
let topDir = ""
let shutdownServer
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
medusaProcess = await setupServer({
cwd,
uploadDir: __dirname,
env,
verbose: true,
})
})
afterAll(async () => {
if (topDir !== "") {
await fs.rm(resolve(__dirname, topDir), { recursive: true })
}
const db = useDb()
await db.shutdown()
await medusaProcess.kill()
await shutdownServer()
})
beforeEach(async () => {
const container = getContainer()
await createDefaultRuleTypes(container)
await productSeeder(dbConnection)
await adminSeeder(dbConnection)
await userSeeder(dbConnection)
await simpleSalesChannelFactory(dbConnection, {
id: "test-channel",
is_default: true,
})
})
afterEach(async () => {
const db = useDb()
await db.teardown()
// @ts-ignore
try {
const isFileExists = (await fs.stat(exportFilePath))?.isFile()
if (isFileExists) {
const [, relativeRoot] = exportFilePath
.replace(__dirname, "")
.split(sep)
if ((await fs.stat(resolve(__dirname, relativeRoot)))?.isDirectory()) {
topDir = relativeRoot
}
await fs.unlink(exportFilePath)
}
} catch (err) {
// noop
}
})
it("should export a csv file containing the expected products", async () => {
const api = useApi()
const productPayload = {
title: "Test export product",
description: "test-product-description",
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 the expected products including new line char in the cells", async () => {
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 () => {
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")
})
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],
}),
])
)
})
})

View File

@@ -0,0 +1,406 @@
import fs from "fs"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { simpleProductFactory } from "../../../../factories"
import { simpleProductCollectionFactory } from "../../../../factories/simple-product-collection-factory"
import adminSeeder from "../../../../helpers/admin-seeder"
import batchJobSeeder from "../../../../helpers/batch-job-seeder"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
const setupServer = require("../../../../environment-helpers/setup-server")
const userSeeder = require("../../../../helpers/user-seeder")
const adminReqConfig = {
headers: {
"x-medusa-access-token": "test_token",
},
}
function getImportFile() {
return path.resolve("__tests__", "product", "admin", "product-import.csv")
}
function copyTemplateFile() {
const csvTemplate = path.resolve(
"__tests__",
"product",
"admin",
"product-import-template.csv"
)
const destination = getImportFile()
fs.copyFileSync(csvTemplate, destination)
}
jest.setTimeout(1000000)
function cleanTempData() {
// cleanup tmp ops files
const opsFiles = path.resolve("__tests__", "product", "admin", "imports")
fs.rmSync(opsFiles, { recursive: true, force: true })
}
const env: Record<any, any> = {
MEDUSA_FF_MEDUSA_V2: true,
}
describe("Product import batch job", () => {
let dbConnection
let shutdownServer
let medusaProcess
const collectionHandle1 = "test-collection1"
const collectionHandle2 = "test-collection2"
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
env.UPLOAD_DIR = __dirname
cleanTempData() // cleanup if previous process didn't manage to do it
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
medusaProcess = await setupServer({
cwd,
uploadDir: __dirname,
env,
verbose: true,
})
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
cleanTempData()
await medusaProcess.kill()
await shutdownServer()
})
beforeEach(async () => {
const container = getContainer()
await createDefaultRuleTypes(container)
await batchJobSeeder(dbConnection)
await adminSeeder(dbConnection)
await userSeeder(dbConnection)
await simpleProductCollectionFactory(dbConnection, [
{
handle: collectionHandle1,
},
{
handle: collectionHandle2,
},
])
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should import a csv file", async () => {
jest.setTimeout(1000000)
const api = useApi()
copyTemplateFile()
const existingProductToBeUpdated = await simpleProductFactory(
dbConnection,
{
id: "existing-product-id",
title: "Test product",
options: [{ id: "opt-1-id", title: "Size" }],
variants: [
{
id: "existing-variant-id",
title: "Initial tile",
sku: "test-sku-4",
options: [
{
option_id: "opt-1-id",
value: "Large",
},
],
},
],
}
)
const response = await api.post(
"/admin/batch-jobs",
{
type: "product-import",
context: {
fileKey: "product-import.csv",
},
},
adminReqConfig
)
const batchJobId = response.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")
const productsResponse = await api.get("/admin/products", adminReqConfig)
expect(productsResponse.data.count).toBe(3)
expect(productsResponse.data.products).toEqual(
expect.arrayContaining([
// NEW PRODUCT
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",
is_giftcard: false,
status: "draft",
thumbnail: "test-image.png",
variants: [
// NEW VARIANT
expect.objectContaining({
title: "Test variant",
sku: "test-sku-1",
barcode: "test-barcode-1",
ean: null,
upc: null,
// inventory_quantity: 10,
prices: expect.arrayContaining([
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.arrayContaining([
expect.objectContaining({
url: "test-image.png",
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "test-option-1",
}),
expect.objectContaining({
title: "test-option-2",
}),
]),
tags: expect.arrayContaining([
expect.objectContaining({
value: "123_1",
}),
]),
collection: expect.objectContaining({
handle: collectionHandle1,
}),
}),
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.arrayContaining([
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.arrayContaining([
expect.objectContaining({
url: "test-image.png",
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "test-option-1",
}),
expect.objectContaining({
title: "test-option-2",
}),
]),
tags: [],
collection: expect.objectContaining({
handle: collectionHandle1,
}),
}),
// // UPDATED PRODUCT
expect.objectContaining({
id: existingProductToBeUpdated?.id,
title: "Test product",
description: "test-product-description",
handle: "test-product-product-2",
is_giftcard: false,
status: "draft",
thumbnail: "test-image.png",
profile_id: expect.any(String),
variants: expect.arrayContaining([
// UPDATED VARIANT
expect.objectContaining({
id: "existing-variant-id",
title: "Test variant changed",
sku: "test-sku-4",
barcode: "test-barcode-4",
options: [
expect.objectContaining({
value: "Large",
option_id: "opt-1-id",
}),
],
}),
// CREATED VARIANT
expect.objectContaining({
title: "Test variant",
product_id: existingProductToBeUpdated.id,
sku: "test-sku-2",
barcode: "test-barcode-2",
ean: null,
upc: null,
// inventory_quantity: 10,
allow_backorder: false,
manage_inventory: true,
prices: [
expect.objectContaining({
currency_code: "dkk",
amount: 110,
region_id: "region-product-import-2",
}),
],
options: [
expect.objectContaining({
value: "Small",
option_id: "opt-1-id",
}),
],
}),
// CREATED VARIANT
expect.objectContaining({
title: "Test variant",
product_id: existingProductToBeUpdated.id,
sku: "test-sku-3",
barcode: "test-barcode-3",
ean: null,
upc: null,
// inventory_quantity: 10,
allow_backorder: false,
manage_inventory: true,
prices: [
expect.objectContaining({
currency_code: "usd",
amount: 120,
region_id: null,
}),
],
options: [
expect.objectContaining({
value: "Medium",
option_id: "opt-1-id",
}),
],
}),
]),
images: [
expect.objectContaining({
url: "test-image.png",
}),
],
options: [
expect.objectContaining({
product_id: existingProductToBeUpdated.id,
id: "opt-1-id",
title: "Size",
}),
],
type: expect.objectContaining({ value: "test-type" }),
tags: [
expect.objectContaining({
value: "123",
}),
],
collection: expect.objectContaining({
handle: collectionHandle2,
}),
}),
])
)
})
})

View File

@@ -0,0 +1,6 @@
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
,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,,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
,test-product-product-1-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,,,TRUE,,,Test variant,test-sku-1-1,test-barcode-1-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
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,test-image.png,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-2,test-barcode-2,10,FALSE,TRUE,,,,,,,,,,,,1.10,Size,Small,,,test-image.png
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,,Test variant,test-sku-3,test-barcode-3,10,FALSE,TRUE,,,,,,,,,,1.20,,,Size,Medium,,,test-image.png
existing-product-id,test-product-product-2,Test product,,test-product-description,draft,,,,,,,,,,Test collection,test-collection2,test-type,123,TRUE,,existing-variant-id,Test variant changed,test-sku-4,test-barcode-4,10,FALSE,TRUE,,,,,,,,,,,,,Size,Large,,,test-image.png
1 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
2 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 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
3 test-product-product-1-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 TRUE Test variant test-sku-1-1 test-barcode-1-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
4 existing-product-id test-product-product-2 Test product test-product-description draft test-image.png Test collection test-collection2 test-type 123 TRUE Test variant test-sku-2 test-barcode-2 10 FALSE TRUE 1.10 Size Small test-image.png
5 existing-product-id test-product-product-2 Test product test-product-description draft Test collection test-collection2 test-type 123 TRUE Test variant test-sku-3 test-barcode-3 10 FALSE TRUE 1.20 Size Medium test-image.png
6 existing-product-id test-product-product-2 Test product test-product-description draft Test collection test-collection2 test-type 123 TRUE existing-variant-id Test variant changed test-sku-4 test-barcode-4 10 FALSE TRUE Size Large test-image.png

View File

@@ -0,0 +1,96 @@
import { AbstractFileService } from "@medusajs/medusa"
import * as fs from "fs"
import mkdirp from "mkdirp"
import { resolve } from "path"
import stream from "stream"
export default class LocalFileService extends AbstractFileService {
constructor({}, options) {
super({}, options)
this.upload_dir_ =
process.env.UPLOAD_DIR ?? options.upload_dir ?? "uploads/images"
if (!fs.existsSync(this.upload_dir_)) {
fs.mkdirSync(this.upload_dir_)
}
}
upload(file) {
return new Promise((resolvePromise, reject) => {
const path = resolve(this.upload_dir_, file.originalname)
let content = ""
if (file.filename) {
content = fs.readFileSync(
resolve(process.cwd(), "uploads", file.filename)
)
}
const pathSegments = path.split("/")
pathSegments.splice(-1)
const dirname = pathSegments.join("/")
mkdirp.sync(dirname, { recursive: true })
fs.writeFile(path, content.toString(), (err) => {
if (err) {
reject(err)
}
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 }) {
const fileKey = `${name}.${ext}`
const path = resolve(this.upload_dir_, fileKey)
const isFileExists = fs.existsSync(path)
if (!isFileExists) {
await this.upload({ originalname: fileKey })
}
const pass = new stream.PassThrough()
pass.pipe(fs.createWriteStream(path))
return {
writeStream: pass,
promise: Promise.resolve(),
url: `${this.upload_dir_}/${fileKey}`,
fileKey,
}
}
async getDownloadStream({ fileKey }) {
return new Promise((resolvePromise, reject) => {
try {
const path = resolve(this.upload_dir_, fileKey)
const data = fs.readFileSync(path)
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}`
}
}