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

This commit is contained in:
Riqwan Thamir authored and GitHub committed 2023-12-12 12:09:25 +00:00
1 parent 6f96ced40f
commit 07107f3565
30 files changed
+1601 -357

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/orchestration": patch
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
---
feat(orchestration,core-flows,medusa): product import uses workflows
@@ -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],
}),
])
)
})
})
@@ -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,
}),
}),
])
)
})
})
@@ -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
@@ -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}`
}
}
@@ -23,7 +23,6 @@ export async function createProductVariantsPrepareData({
container, container,
data, data,
}: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO>): Promise<CreateProductVariantsPreparedData> { }: WorkflowArguments<WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO>): Promise<CreateProductVariantsPreparedData> {
const featureFlagRouter = container.resolve("featureFlagRouter")
const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] = const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] =
data.productVariants || [] data.productVariants || []
@@ -18,7 +18,10 @@ export async function updateProductVariants({
const productModuleService: ProductTypes.IProductModuleService = const productModuleService: ProductTypes.IProductModuleService =
container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName)
for (const [product_id, variantsUpdateData = []] of productVariantsMap) { for (const [
product_id,
variantsUpdateData = [],
] of productVariantsMap) {
updateVariantsData.push( updateVariantsData.push(
...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map( ...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map(
(update) => ({ ...update, product_id }) (update) => ({ ...update, product_id })
@@ -95,7 +95,9 @@ export async function upsertVariantPrices({
min_quantity: price.min_quantity, min_quantity: price.min_quantity,
max_quantity: price.max_quantity, max_quantity: price.max_quantity,
amount: price.amount, amount: price.amount,
currency_code: region_currency_code ?? price.currency_code, currency_code: (
region_currency_code ?? price.currency_code
).toLowerCase(),
} }
moneyAmountsToUpdate.push(priceToUpdate) moneyAmountsToUpdate.push(priceToUpdate)
@@ -104,7 +106,9 @@ export async function upsertVariantPrices({
min_quantity: price.min_quantity, min_quantity: price.min_quantity,
max_quantity: price.max_quantity, max_quantity: price.max_quantity,
amount: price.amount, amount: price.amount,
currency_code: region_currency_code ?? price.currency_code, currency_code: (
region_currency_code ?? price.currency_code
).toLowerCase(),
rules: region_rules ?? {}, rules: region_rules ?? {},
} }
@@ -19,7 +19,7 @@ import { pickBy } from "lodash"
import { ProductStatus } from "../../../../models" import { ProductStatus } from "../../../../models"
import PriceListService from "../../../../services/price-list" import PriceListService from "../../../../services/price-list"
import { FilterableProductProps } from "../../../../types/product" import { FilterableProductProps } from "../../../../types/product"
import { listAndCountProductWithIsolatedProductModule } from "../products/list-products" import { listProducts } from "../../../../utils"
/** /**
* @oas [get] /admin/price-lists/{id}/products * @oas [get] /admin/price-lists/{id}/products
@@ -196,8 +196,8 @@ export default async (req: Request, res) => {
} }
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
;[products, count] = await listAndCountProductWithIsolatedProductModule( ;[products, count] = await listProducts(
req, req.scope,
filterableFields, filterableFields,
req.listConfig req.listConfig
) )
@@ -1,5 +1,5 @@
import { Workflows, createProducts } from "@medusajs/core-flows"
import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import { createProducts, Workflows } from "@medusajs/core-flows"
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
@@ -45,7 +45,7 @@ import { EntityManager } from "typeorm"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { ProductStatus } from "../../../../models" import { ProductStatus } from "../../../../models"
import { Logger } from "../../../../types/global" import { Logger } from "../../../../types/global"
import { validator } from "../../../../utils" import { retrieveProduct, validator } from "../../../../utils"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
/** /**
@@ -257,7 +257,11 @@ export default async (req, res) => {
let rawProduct let rawProduct
if (isMedusaV2Enabled) { if (isMedusaV2Enabled) {
rawProduct = await getProductWithIsolatedProductModule(req, product.id) rawProduct = await retrieveProduct(
req.scope,
product.id,
defaultAdminProductRemoteQueryObject
)
} else { } else {
rawProduct = await productService.retrieve(product.id, { rawProduct = await productService.retrieve(product.id, {
select: defaultAdminProductFields, select: defaultAdminProductFields,
@@ -272,26 +276,6 @@ export default async (req, res) => {
res.json({ product: pricedProduct }) res.json({ product: pricedProduct })
} }
async function getProductWithIsolatedProductModule(req, id) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const [product] = await remoteQuery(query)
product.profile_id = product.profile?.id
return product
}
class ProductVariantOptionReq { class ProductVariantOptionReq {
@IsString() @IsString()
value: string value: string
@@ -1,6 +1,7 @@
import { CreateProductVariants } from "@medusajs/core-flows"
import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { IInventoryService, WorkflowTypes } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import { CreateProductVariants } from "@medusajs/core-flows" import { Type } from "class-transformer"
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
@@ -10,7 +11,12 @@ import {
IsString, IsString,
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { EntityManager } from "typeorm"
import {
defaultAdminProductFields,
defaultAdminProductRelations,
defaultAdminProductRemoteQueryObject,
} from "."
import { import {
PricingService, PricingService,
ProductService, ProductService,
@@ -21,10 +27,7 @@ import {
CreateProductVariantInput, CreateProductVariantInput,
ProductVariantPricesCreateReq, ProductVariantPricesCreateReq,
} from "../../../../types/product-variant" } from "../../../../types/product-variant"
import { Type } from "class-transformer" import { retrieveProduct, validator } from "../../../../utils"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { getProductWithIsolatedProductModule } from "./get-product"
import { createVariantsTransaction } from "./transaction/create-product-variant" import { createVariantsTransaction } from "./transaction/create-product-variant"
/** /**
@@ -157,10 +160,10 @@ export default async (req, res) => {
}, },
}) })
rawProduct = await getProductWithIsolatedProductModule( rawProduct = await retrieveProduct(
req, req.scope,
id, id,
req.retrieveConfig defaultAdminProductRemoteQueryObject
) )
} else { } else {
await manager.transaction(async (transactionManager) => { await manager.transaction(async (transactionManager) => {
@@ -5,8 +5,9 @@ import {
SalesChannelService, SalesChannelService,
} from "../../../../services" } from "../../../../services"
import { MedusaError, MedusaV2Flag, promiseAll } from "@medusajs/utils" import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { FindParams } from "../../../../types/common" import { FindParams } from "../../../../types/common"
import { retrieveProduct } from "../../../../utils"
import { defaultAdminProductRemoteQueryObject } from "./index" import { defaultAdminProductRemoteQueryObject } from "./index"
/** /**
@@ -76,10 +77,10 @@ export default async (req, res) => {
let rawProduct let rawProduct
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
rawProduct = await getProductWithIsolatedProductModule( rawProduct = await retrieveProduct(
req, req.scope,
id, id,
req.retrieveConfig defaultAdminProductRemoteQueryObject
) )
} else { } else {
rawProduct = await productService.retrieve(id, req.retrieveConfig) rawProduct = await productService.retrieve(id, req.retrieveConfig)
@@ -118,35 +119,4 @@ export default async (req, res) => {
res.json({ product }) res.json({ product })
} }
export async function getProductWithIsolatedProductModule(
req,
id,
retrieveConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const [product] = await remoteQuery(query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${id} not found`
)
}
product.profile_id = product.profile?.id
return product
}
export class AdminGetProductParams extends FindParams {} export class AdminGetProductParams extends FindParams {}
@@ -1,23 +1,19 @@
import { IsNumber, IsOptional, IsString } from "class-validator" import { IsNumber, IsOptional, IsString } from "class-validator"
import { import {
PriceListService,
PricingService, PricingService,
ProductService, ProductService,
ProductVariantInventoryService, ProductVariantInventoryService,
SalesChannelService, SalesChannelService,
} from "../../../../services" } from "../../../../services"
import { import { listProducts } from "../../../../utils"
IInventoryService,
IPricingModuleService, import { IInventoryService } from "@medusajs/types"
IProductModuleService, import { MedusaV2Flag } from "@medusajs/utils"
} from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { Product } from "../../../../models" import { Product } from "../../../../models"
import { PricedProduct } from "../../../../types/pricing" import { PricedProduct } from "../../../../types/pricing"
import { FilterableProductProps } from "../../../../types/product" import { FilterableProductProps } from "../../../../types/product"
import { defaultAdminProductRemoteQueryObject } from "./index"
/** /**
* @oas [get] /admin/products * @oas [get] /admin/products
@@ -252,12 +248,11 @@ export default async (req, res) => {
let count let count
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const [products, count_] = const [products, count_] = await listProducts(
await listAndCountProductWithIsolatedProductModule( req.scope,
req, req.filterableFields,
req.filterableFields, req.listConfig
req.listConfig )
)
rawProducts = products rawProducts = products
count = count_ count = count_
@@ -305,171 +300,6 @@ export default async (req, res) => {
}) })
} }
async function getVariantsFromPriceList(req, priceListId) {
const remoteQuery = req.scope.resolve("remoteQuery")
const pricingModuleService: IPricingModuleService = req.scope.resolve(
"pricingModuleService"
)
const productModuleService: IProductModuleService = req.scope.resolve(
"productModuleService"
)
const [priceList] = await pricingModuleService.listPriceLists(
{ id: [priceListId] },
{
relations: [
"price_set_money_amounts",
"price_set_money_amounts.price_set",
],
select: ["price_set_money_amounts.price_set.id"],
}
)
const priceSetIds = priceList.price_set_money_amounts?.map(
(psma) => psma.price_set?.id
)
const query = {
product_variant_price_set: {
__args: {
price_set_id: priceSetIds,
},
fields: ["variant_id", "price_set_id"],
},
}
const variantPriceSets = await remoteQuery(query)
const variantIds = variantPriceSets.map((vps) => vps.variant_id)
return await productModuleService.listVariants(
{
id: variantIds,
},
{
select: ["product_id"],
}
)
}
export async function listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
const promises: Promise<void>[] = []
// This is not the best way of handling cross filtering but for now I would say it is fine
const salesChannelIdFilter = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id
if (salesChannelIdFilter) {
const salesChannelService = req.scope.resolve(
"salesChannelService"
) as SalesChannelService
promises.push(
salesChannelService
.listProductIdsBySalesChannelIds(salesChannelIdFilter)
.then((productIdsInSalesChannel) => {
let filteredProductIds =
productIdsInSalesChannel[salesChannelIdFilter]
if (filterableFields.id) {
filterableFields.id = Array.isArray(filterableFields.id)
? filterableFields.id
: [filterableFields.id]
const salesChannelProductIdsSet = new Set(filteredProductIds)
filteredProductIds = filterableFields.id.filter((productId) =>
salesChannelProductIdsSet.has(productId)
)
}
filteredProductIds.map((id) => productIdsFilter.add(id))
})
)
}
const priceListId = filterableFields.price_list_id
delete filterableFields.price_list_id
if (priceListId) {
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(req, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = req.scope.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
delete filterableFields.discount_condition_id
if (discountConditionId) {
// TODO implement later
}
await promiseAll(promises)
if (productIdsFilter.size > 0) {
filterableFields.id = Array.from(productIdsFilter)
}
if (variantIdsFilter.size > 0) {
filterableFields.variants = { id: Array.from(variantIdsFilter) }
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const {
rows: products,
metadata: { count },
} = await remoteQuery(query)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
/** /**
* Parameters used to filter and configure the pagination of the retrieved products. * Parameters used to filter and configure the pagination of the retrieved products.
*/ */
@@ -1,3 +1,4 @@
import { Workflows, updateProducts } from "@medusajs/core-flows"
import { DistributedTransaction } from "@medusajs/orchestration" import { DistributedTransaction } from "@medusajs/orchestration"
import { import {
FlagRouter, FlagRouter,
@@ -5,7 +6,6 @@ import {
MedusaV2Flag, MedusaV2Flag,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import { updateProducts, Workflows } from "@medusajs/core-flows"
import { Type } from "class-transformer" import { Type } from "class-transformer"
import { import {
IsArray, IsArray,
@@ -45,6 +45,7 @@ import {
ProductVariantPricesUpdateReq, ProductVariantPricesUpdateReq,
UpdateProductVariantInput, UpdateProductVariantInput,
} from "../../../../types/product-variant" } from "../../../../types/product-variant"
import { retrieveProduct } from "../../../../utils"
import { import {
createVariantsTransaction, createVariantsTransaction,
revertVariantTransaction, revertVariantTransaction,
@@ -292,7 +293,11 @@ export default async (req, res) => {
let rawProduct let rawProduct
if (isMedusaV2Enabled) { if (isMedusaV2Enabled) {
rawProduct = await getProductWithIsolatedProductModule(req, id) rawProduct = await retrieveProduct(
req.scope,
id,
defaultAdminProductRemoteQueryObject
)
} else { } else {
rawProduct = await productService.retrieve(id, { rawProduct = await productService.retrieve(id, {
select: defaultAdminProductFields, select: defaultAdminProductFields,
@@ -305,26 +310,6 @@ export default async (req, res) => {
res.json({ product }) res.json({ product })
} }
async function getProductWithIsolatedProductModule(req, id) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const [product] = await remoteQuery(query)
product.profile_id = product.profile?.id
return product
}
class ProductVariantOptionReq { class ProductVariantOptionReq {
@IsString() @IsString()
value: string value: string
@@ -1,7 +1,10 @@
import { FlagRouter } from "@medusajs/utils" import { FlagRouter } from "@medusajs/utils"
import { Request } from "express" import { Request } from "express"
import { IdMap, MockManager } from "medusa-test-utils" import { IdMap, MockManager } from "medusa-test-utils"
import { AdminPostBatchesReq, defaultAdminProductRelations, } from "../../../../api" import {
AdminPostBatchesReq,
defaultAdminProductRelations,
} from "../../../../api"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { User } from "../../../../models" import { User } from "../../../../models"
import { BatchJobStatus } from "../../../../types/batch-job" import { BatchJobStatus } from "../../../../types/batch-job"
@@ -117,6 +120,7 @@ describe("Product export strategy", () => {
batchJobService: batchJobServiceMock as any, batchJobService: batchJobServiceMock as any,
productService: productServiceMock as any, productService: productServiceMock as any,
featureFlagRouter: new FlagRouter({}), featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
}) })
it("should generate the appropriate template", async () => { it("should generate the appropriate template", async () => {
@@ -264,6 +268,7 @@ describe("Product export strategy", () => {
productService: productServiceWithNoDataMock as any, productService: productServiceWithNoDataMock as any,
manager: MockManager, manager: MockManager,
featureFlagRouter: new FlagRouter({}), featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
}) })
await productExportStrategy.prepareBatchJobForProcessing( await productExportStrategy.prepareBatchJobForProcessing(
@@ -283,6 +288,7 @@ describe("Product export strategy", () => {
productService: productServiceMock as any, productService: productServiceMock as any,
manager: MockManager, manager: MockManager,
featureFlagRouter: new FlagRouter({}), featureFlagRouter: new FlagRouter({}),
remoteQuery: (() => {}) as any,
}) })
await productExportStrategy.prepareBatchJobForProcessing( await productExportStrategy.prepareBatchJobForProcessing(
@@ -381,6 +387,7 @@ describe("Product export strategy with sales Channels", () => {
fileService: fileServiceMock as any, fileService: fileServiceMock as any,
batchJobService: batchJobServiceMock as any, batchJobService: batchJobServiceMock as any,
productService: productServiceMock as any, productService: productServiceMock as any,
remoteQuery: (() => {}) as any,
featureFlagRouter: new FlagRouter({ featureFlagRouter: new FlagRouter({
[SalesChannelFeatureFlag.key]: true, [SalesChannelFeatureFlag.key]: true,
}), }),
@@ -1,8 +1,7 @@
import { EntityManager } from "typeorm" import { MedusaContainer } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag, createContainerLike } from "@medusajs/utils"
import { humanizeAmount } from "medusa-core-utils" import { humanizeAmount } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { FlagRouter } from "@medusajs/utils"
import { defaultAdminProductRelations } from "../../../api" import { defaultAdminProductRelations } from "../../../api"
import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces" import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories" import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories"
@@ -11,19 +10,19 @@ import { Product, ProductVariant } from "../../../models"
import { BatchJobService, ProductService } from "../../../services" import { BatchJobService, ProductService } from "../../../services"
import { BatchJobStatus, CreateBatchJobInput } from "../../../types/batch-job" import { BatchJobStatus, CreateBatchJobInput } from "../../../types/batch-job"
import { FindProductConfig } from "../../../types/product" import { FindProductConfig } from "../../../types/product"
import { csvCellContentFormatter } from "../../../utils" import { csvCellContentFormatter, listProducts } from "../../../utils"
import { prepareListQuery } from "../../../utils/get-query-config" import { prepareListQuery } from "../../../utils/get-query-config"
import { import {
DynamicProductExportDescriptor, DynamicProductExportDescriptor,
ProductExportBatchJob, ProductExportBatchJob,
ProductExportBatchJobContext, ProductExportBatchJobContext,
ProductExportInjectedDependencies, ProductExportInjectedDependencies,
ProductExportPriceData, ProductExportPriceData,
} from "./types" } from "./types"
import { import {
productCategoriesColumnsDefinition, productCategoriesColumnsDefinition,
productColumnsDefinition, productColumnsDefinition,
productSalesChannelColumnsDefinition, productSalesChannelColumnsDefinition,
} from "./types/columns-definition" } from "./types/columns-definition"
export default class ProductExportStrategy extends AbstractBatchJobStrategy { export default class ProductExportStrategy extends AbstractBatchJobStrategy {
@@ -37,6 +36,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
protected readonly productService_: ProductService protected readonly productService_: ProductService
protected readonly fileService_: IFileService protected readonly fileService_: IFileService
protected readonly featureFlagRouter_: FlagRouter protected readonly featureFlagRouter_: FlagRouter
protected readonly remoteQuery_: any
protected readonly defaultRelations_ = [ protected readonly defaultRelations_ = [
...defaultAdminProductRelations, ...defaultAdminProductRelations,
@@ -68,6 +68,7 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
productService, productService,
fileService, fileService,
featureFlagRouter, featureFlagRouter,
remoteQuery,
}: ProductExportInjectedDependencies) { }: ProductExportInjectedDependencies) {
super({ super({
manager, manager,
@@ -75,8 +76,10 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
productService, productService,
fileService, fileService,
featureFlagRouter, featureFlagRouter,
remoteQuery,
}) })
this.remoteQuery_ = remoteQuery
this.manager_ = manager this.manager_ = manager
this.batchJobService_ = batchJobService this.batchJobService_ = batchJobService
this.productService_ = productService this.productService_ = productService
@@ -135,6 +138,10 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
} }
async preProcessBatchJob(batchJobId: string): Promise<void> { async preProcessBatchJob(batchJobId: string): Promise<void> {
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
return await this.atomicPhase_(async (transactionManager) => { return await this.atomicPhase_(async (transactionManager) => {
const batchJob = (await this.batchJobService_ const batchJob = (await this.batchJobService_
.withTransaction(transactionManager) .withTransaction(transactionManager)
@@ -144,12 +151,29 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
const limit = batchJob.context?.list_config?.take ?? this.DEFAULT_LIMIT const limit = batchJob.context?.list_config?.take ?? this.DEFAULT_LIMIT
const { list_config = {}, filterable_fields = {} } = batchJob.context const { list_config = {}, filterable_fields = {} } = batchJob.context
const [productList, count] = await this.productService_ let productList: Product[] = []
.withTransaction(transactionManager) let count = 0
.listAndCount(filterable_fields, { const container = createContainerLike(
...(list_config ?? {}), this.__container__
take: Math.min(batchJob.context.batch_size ?? Infinity, limit), ) as MedusaContainer
} as FindProductConfig)
if (isMedusaV2Enabled) {
;[productList, count] = await listProducts(
container,
filterable_fields,
{
...list_config,
take: null,
}
)
} else {
;[productList, count] = await this.productService_
.withTransaction(transactionManager)
.listAndCount(filterable_fields, {
...(list_config ?? {}),
take: Math.min(batchJob.context.batch_size ?? Infinity, limit),
} as FindProductConfig)
}
const productCount = batchJob.context?.batch_size ?? count const productCount = batchJob.context?.batch_size ?? count
let products: Product[] = productList let products: Product[] = productList
@@ -162,13 +186,25 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
while (offset < productCount) { while (offset < productCount) {
if (!products?.length) { if (!products?.length) {
products = await this.productService_ if (isMedusaV2Enabled) {
.withTransaction(transactionManager) ;[productList, count] = await listProducts(
.list(filterable_fields, { container,
...list_config, filterable_fields,
skip: offset, {
take: Math.min(productCount - offset, limit), ...list_config,
} as FindProductConfig) skip: offset,
take: Math.min(productCount - offset, limit),
}
)
} else {
products = await this.productService_
.withTransaction(transactionManager)
.list(filterable_fields, {
...list_config,
skip: offset,
take: Math.min(productCount - offset, limit),
} as FindProductConfig)
}
} }
const shapeData = this.getProductRelationsDynamicColumnsShape(products) const shapeData = this.getProductRelationsDynamicColumnsShape(products)
@@ -669,7 +705,8 @@ export default class ProductExportStrategy extends AbstractBatchJobStrategy {
if ( if (
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
) { ) {
const salesChannelCount = product?.sales_channels?.length ?? 0 const salesChannelCount =
(product as Product)?.sales_channels?.length ?? 0
salesChannelsColumnCount = Math.max( salesChannelsColumnCount = Math.max(
salesChannelsColumnCount, salesChannelsColumnCount,
salesChannelCount salesChannelCount
@@ -1,8 +1,14 @@
/* eslint-disable valid-jsdoc */ /* eslint-disable valid-jsdoc */
import { computerizeAmount, MedusaError } from "medusa-core-utils" import {
CreateProductVariants,
UpdateProductVariants,
createProducts,
updateProducts,
} from "@medusajs/core-flows"
import { ProductWorkflow } from "@medusajs/types"
import { FlagRouter, MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { MedusaError, computerizeAmount } from "medusa-core-utils"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { FlagRouter, promiseAll } from "@medusajs/utils"
import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces" import { AbstractBatchJobStrategy, IFileService } from "../../../interfaces"
import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories" import ProductCategoryFeatureFlag from "../../../loaders/feature-flags/product-categories"
import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels" import SalesChannelFeatureFlag from "../../../loaders/feature-flags/sales-channels"
@@ -109,7 +115,6 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
}) })
this.featureFlagRouter_ = featureFlagRouter this.featureFlagRouter_ = featureFlagRouter
this.manager_ = manager this.manager_ = manager
this.fileService_ = fileService this.fileService_ = fileService
this.batchJobService_ = batchJobService this.batchJobService_ = batchJobService
@@ -168,7 +173,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
const variantsUpdate: TParsedProductImportRowData[] = [] const variantsUpdate: TParsedProductImportRowData[] = []
for (const row of csvData) { for (const row of csvData) {
if ((row["variant.prices"] as Record<string, any>[]).length) { if ((row["variant.prices"] as Record<string, any>[])?.length) {
await this.prepareVariantPrices(row) await this.prepareVariantPrices(row)
} }
@@ -278,6 +283,7 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
let totalOperationCount = 0 let totalOperationCount = 0
const operationsCounts = {} const operationsCounts = {}
Object.keys(ops).forEach((key) => { Object.keys(ops).forEach((key) => {
operationsCounts[key] = ops[key].length operationsCounts[key] = ops[key].length
totalOperationCount += ops[key].length totalOperationCount += ops[key].length
@@ -417,6 +423,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return return
} }
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile( const productOps = await this.downloadImportOpsFile(
@@ -468,10 +478,27 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
) )
} }
// TODO: we should only pass the expected data and should not have to cast the entire object. Here we are passing everything contained in productData if (isMedusaV2Enabled) {
await productServiceTx.create( const createProductWorkflow = createProducts(this.__container__)
productData as unknown as CreateProductInput
) const input = {
products: [
productData,
] as unknown as ProductWorkflow.CreateProductInputDTO[],
}
await createProductWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
// TODO: we should only pass the expected data and should not have to cast the entire object. Here we are passing everything contained in productData
await productServiceTx.create(
productData as unknown as CreateProductInput
)
}
} catch (e) { } catch (e) {
ProductImportStrategy.throwDescriptiveError(productOp, e.message) ProductImportStrategy.throwDescriptiveError(productOp, e.message)
} }
@@ -490,6 +517,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return return
} }
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const productOps = await this.downloadImportOpsFile( const productOps = await this.downloadImportOpsFile(
batchJob, batchJob,
@@ -541,11 +572,31 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
) )
} }
// TODO: we should only pass the expected data. Here we are passing everything contained in productData if (isMedusaV2Enabled) {
await productServiceTx.update( const updateProductWorkflow = updateProducts(this.__container__)
productOp["product.id"] as string,
productData const input = {
) products: [
{
id: productOp["product.id"] as string,
...productData,
},
],
}
await updateProductWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
// TODO: we should only pass the expected data. Here we are passing everything contained in productData
await productServiceTx.update(
productOp["product.id"] as string,
productData
)
}
} catch (e) { } catch (e) {
ProductImportStrategy.throwDescriptiveError(productOp, e.message) ProductImportStrategy.throwDescriptiveError(productOp, e.message)
} }
@@ -565,6 +616,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return return
} }
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile( const variantOps = await this.downloadImportOpsFile(
@@ -600,9 +655,30 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
delete variant.id delete variant.id
delete variant.product delete variant.product
await this.productVariantService_ if (isMedusaV2Enabled) {
.withTransaction(transactionManager) const createProductVariantsWorkflow =
.create(product!, variant as unknown as CreateProductVariantInput) CreateProductVariants.createProductVariants(this.__container__)
const input: ProductWorkflow.CreateProductVariantsWorkflowInputDTO = {
productVariants: [
{
...variant,
product_id: product.id,
},
],
}
await createProductVariantsWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
await this.productVariantService_
.withTransaction(transactionManager)
.create(product!, variant as unknown as CreateProductVariantInput)
}
await this.updateProgress(batchJob.id) await this.updateProgress(batchJob.id)
} catch (e) { } catch (e) {
@@ -621,6 +697,10 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
return return
} }
const isMedusaV2Enabled = this.featureFlagRouter_.isFeatureEnabled(
MedusaV2Flag.key
)
const transactionManager = this.transactionManager_ ?? this.manager_ const transactionManager = this.transactionManager_ ?? this.manager_
const variantOps = await this.downloadImportOpsFile( const variantOps = await this.downloadImportOpsFile(
@@ -637,15 +717,36 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
variantOp["product.handle"] as string variantOp["product.handle"] as string
) )
await this.prepareVariantOptions(variantOp, product.id) await this.prepareVariantOptions(variantOp, product.id!)
const updateData = transformVariantData(variantOp) const updateData = transformVariantData(variantOp)
delete updateData.product delete updateData.product
delete updateData["product.handle"] delete updateData["product.handle"]
await this.productVariantService_ if (isMedusaV2Enabled) {
.withTransaction(transactionManager) const updateProductVariantsWorkflow =
.update(variantOp["variant.id"] as string, updateData) UpdateProductVariants.updateProductVariants(this.__container__)
const input: ProductWorkflow.UpdateProductVariantsWorkflowInputDTO = {
productVariants: [
{
id: variantOp["variant.id"] as string,
...updateData,
},
],
}
await updateProductVariantsWorkflow.run({
input,
context: {
manager: transactionManager,
},
})
} else {
await this.productVariantService_
.withTransaction(transactionManager)
.update(variantOp["variant.id"] as string, updateData)
}
} catch (e) { } catch (e) {
ProductImportStrategy.throwDescriptiveError(variantOp, e.message) ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
} }
@@ -1,3 +1,4 @@
import { RemoteQueryFunction } from "@medusajs/types"
import { FlagRouter } from "@medusajs/utils" import { FlagRouter } from "@medusajs/utils"
import { FileService } from "medusa-interfaces" import { FileService } from "medusa-interfaces"
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
@@ -5,14 +6,14 @@ import { IFileService } from "../../../../interfaces"
import { CsvSchema, CsvSchemaColumn } from "../../../../interfaces/csv-parser" import { CsvSchema, CsvSchemaColumn } from "../../../../interfaces/csv-parser"
import { BatchJob, Product, ProductVariant } from "../../../../models" import { BatchJob, Product, ProductVariant } from "../../../../models"
import { import {
BatchJobService, BatchJobService,
ProductCategoryService, ProductCategoryService,
ProductCollectionService, ProductCollectionService,
ProductService, ProductService,
ProductVariantService, ProductVariantService,
RegionService, RegionService,
SalesChannelService, SalesChannelService,
ShippingProfileService, ShippingProfileService,
} from "../../../../services" } from "../../../../services"
import { Selector } from "../../../../types/common" import { Selector } from "../../../../types/common"
@@ -22,6 +23,7 @@ export type ProductExportInjectedDependencies = {
productService: ProductService productService: ProductService
fileService: IFileService fileService: IFileService
featureFlagRouter: FlagRouter featureFlagRouter: FlagRouter
remoteQuery: RemoteQueryFunction
} }
export type ProductExportBatchJobContext = { export type ProductExportBatchJobContext = {
+1
View File
@@ -11,6 +11,7 @@ export * from "./is-object"
export * from "./is-string" export * from "./is-string"
export * from "./omit-deep" export * from "./omit-deep"
export * from "./product-category" export * from "./product-category"
export * from "./queries"
export * from "./remote-query-fetch-data" export * from "./remote-query-fetch-data"
export * from "./remove-undefined-properties" export * from "./remove-undefined-properties"
export * from "./set-metadata" export * from "./set-metadata"
@@ -0,0 +1 @@
export * from "./products"
@@ -0,0 +1,34 @@
import { MedusaContainer, ProductVariantDTO } from "@medusajs/types"
export async function getVariantsFromPriceList(
container: MedusaContainer,
priceListId: string
) {
const remoteQuery = container.resolve("remoteQuery")
const query = {
price_list: {
__args: { id: [priceListId] },
price_set_money_amounts: {
price_set: {
variant_link: { variant: { fields: ["id", "product_id"] } },
},
},
},
}
const priceLists = await remoteQuery(query)
const variants: ProductVariantDTO[] = []
priceLists.forEach((priceList) => {
priceList.price_set_money_amounts?.forEach((psma) => {
const variant = psma.price_set?.variant_link?.variant
if (variant) {
variants.push(variant)
}
})
})
return variants
}
@@ -0,0 +1,3 @@
export * from "./get-variants-from-price-list"
export * from "./list-products"
export * from "./retrieve-product"
@@ -0,0 +1,248 @@
import { MedusaContainer } from "@medusajs/types"
import { MedusaV2Flag, promiseAll } from "@medusajs/utils"
import { PriceListService, SalesChannelService } from "../../../services"
import { getVariantsFromPriceList } from "./get-variants-from-price-list"
export async function listProducts(
container: MedusaContainer,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const featureFlagRouter = container.resolve("featureFlagRouter")
const productIdsFilter: Set<string> = new Set()
const variantIdsFilter: Set<string> = new Set()
const promises: Promise<void>[] = []
// This is not the best way of handling cross filtering but for now I would say it is fine
const salesChannelIdFilter = filterableFields.sales_channel_id
delete filterableFields.sales_channel_id
if (salesChannelIdFilter) {
const salesChannelService = container.resolve(
"salesChannelService"
) as SalesChannelService
promises.push(
salesChannelService
.listProductIdsBySalesChannelIds(salesChannelIdFilter)
.then((productIdsInSalesChannel) => {
let filteredProductIds =
productIdsInSalesChannel[salesChannelIdFilter]
if (filterableFields.id) {
filterableFields.id = Array.isArray(filterableFields.id)
? filterableFields.id
: [filterableFields.id]
const salesChannelProductIdsSet = new Set(filteredProductIds)
filteredProductIds = filterableFields.id.filter((productId) =>
salesChannelProductIdsSet.has(productId)
)
}
filteredProductIds.map((id) => productIdsFilter.add(id))
})
)
}
const priceListId = filterableFields.price_list_id
delete filterableFields.price_list_id
if (priceListId) {
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
const variants = await getVariantsFromPriceList(container, priceListId)
variants.forEach((pv) => variantIdsFilter.add(pv.id))
} else {
// TODO: it is working but validate the behaviour.
// e.g pricing context properly set.
// At the moment filtering by price list but not having any customer id or
// include discount forces the query to filter with price list id is null
const priceListService = container.resolve(
"priceListService"
) as PriceListService
promises.push(
priceListService
.listPriceListsVariantIdsMap(priceListId)
.then((priceListVariantIdsMap) => {
priceListVariantIdsMap[priceListId].map((variantId) =>
variantIdsFilter.add(variantId)
)
})
)
}
}
const discountConditionId = filterableFields.discount_condition_id
delete filterableFields.discount_condition_id
if (discountConditionId) {
// TODO implement later
}
await promiseAll(promises)
if (productIdsFilter.size > 0) {
filterableFields.id = Array.from(productIdsFilter)
}
if (variantIdsFilter.size > 0) {
filterableFields.variants = { id: Array.from(variantIdsFilter) }
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
const query = {
product: {
__args: variables,
...defaultAdminProductRemoteQueryObject,
},
}
const {
rows: products,
metadata: { count },
} = await remoteQuery(query)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
export const defaultAdminProductRemoteQueryObject = {
fields: [
"id",
"title",
"subtitle",
"status",
"external_id",
"description",
"handle",
"is_giftcard",
"discountable",
"thumbnail",
"collection_id",
"type_id",
"weight",
"length",
"height",
"width",
"hs_code",
"origin_country",
"mid_code",
"material",
"created_at",
"updated_at",
"deleted_at",
"metadata",
],
images: {
fields: ["id", "created_at", "updated_at", "deleted_at", "url", "metadata"],
},
tags: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
type: {
fields: ["id", "created_at", "updated_at", "deleted_at", "value"],
},
collection: {
fields: ["title", "handle", "id", "created_at", "updated_at", "deleted_at"],
},
categories: {
fields: [
"id",
"name",
"description",
"handle",
"is_active",
"is_internal",
"parent_category_id",
],
},
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"metadata",
],
values: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
variants: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"title",
"product_id",
"sku",
"barcode",
"ean",
"upc",
"variant_rank",
"inventory_quantity",
"allow_backorder",
"manage_inventory",
"hs_code",
"origin_country",
"mid_code",
"material",
"weight",
"length",
"height",
"width",
"metadata",
],
options: {
fields: [
"id",
"created_at",
"updated_at",
"deleted_at",
"value",
"option_id",
"variant_id",
"metadata",
],
},
},
profile: {
fields: ["id", "created_at", "updated_at", "deleted_at", "name", "type"],
},
}
@@ -0,0 +1,28 @@
import { MedusaError } from "@medusajs/utils"
export async function retrieveProduct(container, id, remoteQueryObject = {}) {
// TODO: Add support for fields/expands
const remoteQuery = container.resolve("remoteQuery")
const variables = { id }
const query = {
product: {
__args: variables,
...remoteQueryObject,
},
}
const [product] = await remoteQuery(query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with id: ${id} not found`
)
}
product.profile_id = product.profile?.id
return product
}
@@ -1,9 +1,9 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { WorkflowDefinition, WorkflowManager } from "./workflow-manager" import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import { DistributedTransaction } from "../transaction" import { DistributedTransaction } from "../transaction"
import { asValue } from "awilix" import { WorkflowDefinition, WorkflowManager } from "./workflow-manager"
import { createMedusaContainer } from "@medusajs/utils"
export class GlobalWorkflow extends WorkflowManager { export class GlobalWorkflow extends WorkflowManager {
protected static workflows: Map<string, WorkflowDefinition> = new Map() protected static workflows: Map<string, WorkflowDefinition> = new Map()
@@ -16,17 +16,17 @@ export class GlobalWorkflow extends WorkflowManager {
) { ) {
super() super()
const container = createMedusaContainer() let container
// Medusa container
if (!Array.isArray(modulesLoaded) && modulesLoaded) { if (!Array.isArray(modulesLoaded) && modulesLoaded) {
const cradle = modulesLoaded.cradle if (!("cradle" in modulesLoaded)) {
for (const key in cradle) { container = createContainerLike(modulesLoaded)
container.register(key, asValue(cradle[key])) } else {
container = modulesLoaded
} }
} } else if (Array.isArray(modulesLoaded) && modulesLoaded.length) {
// Array of modules container = createMedusaContainer()
else if (modulesLoaded?.length) {
for (const mod of modulesLoaded) { for (const mod of modulesLoaded) {
const registrationName = mod.__definition.registrationName const registrationName = mod.__definition.registrationName
container.register(registrationName, asValue(mod)) container.register(registrationName, asValue(mod))
@@ -1,5 +1,5 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { createMedusaContainer } from "@medusajs/utils" import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { import {
DistributedTransaction, DistributedTransaction,
@@ -27,7 +27,7 @@ export class LocalWorkflow {
constructor( constructor(
workflowId: string, workflowId: string,
modulesLoaded?: LoadedModule[] | MedusaContainer modulesLoaded: LoadedModule[] | MedusaContainer
) { ) {
const globalWorkflow = WorkflowManager.getWorkflow(workflowId) const globalWorkflow = WorkflowManager.getWorkflow(workflowId)
if (!globalWorkflow) { if (!globalWorkflow) {
@@ -39,17 +39,17 @@ export class LocalWorkflow {
this.workflow = globalWorkflow this.workflow = globalWorkflow
this.handlers = new Map(globalWorkflow.handlers_) this.handlers = new Map(globalWorkflow.handlers_)
const container = createMedusaContainer() let container
// Medusa container
if (!Array.isArray(modulesLoaded) && modulesLoaded) { if (!Array.isArray(modulesLoaded) && modulesLoaded) {
const cradle = modulesLoaded.cradle if (!("cradle" in modulesLoaded)) {
for (const key of Object.keys(cradle ?? {})) { container = createContainerLike(modulesLoaded)
container.register(key, asValue(cradle[key])) } else {
container = modulesLoaded
} }
} } else if (Array.isArray(modulesLoaded) && modulesLoaded.length) {
// Array of modules container = createMedusaContainer()
else if (modulesLoaded?.length) {
for (const mod of modulesLoaded) { for (const mod of modulesLoaded) {
const registrationName = mod.__definition.registrationName const registrationName = mod.__definition.registrationName
container.register(registrationName, asValue(mod)) container.register(registrationName, asValue(mod))
@@ -67,15 +67,15 @@ import {
MedusaError, MedusaError,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
import {
CreateProductOptionValueDTO,
UpdateProductOptionValueDTO,
} from "../types/services/product-option-value"
import { import {
entityNameToLinkableKeysMap, entityNameToLinkableKeysMap,
joinerConfig, joinerConfig,
LinkableKeys, LinkableKeys,
} from "./../joiner-config" } from "./../joiner-config"
import {
CreateProductOptionValueDTO,
UpdateProductOptionValueDTO,
} from "../types/services/product-option-value"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -1063,6 +1063,10 @@ export default class ProductModuleService<
const existingProductVariantsMap = new Map<string, ProductVariant[]>( const existingProductVariantsMap = new Map<string, ProductVariant[]>(
data.map((productData) => { data.map((productData) => {
if (productData.variants === undefined) {
return [productData.id, []]
}
const productVariantsForProduct = existingProductVariants.filter( const productVariantsForProduct = existingProductVariants.filter(
(variant) => variant.product_id === productData.id (variant) => variant.product_id === productData.id
) )
@@ -4,3 +4,7 @@ export type MedusaContainer = AwilixContainer & {
registerAdd: <T>(name: string, registration: T) => MedusaContainer registerAdd: <T>(name: string, registration: T) => MedusaContainer
createScope: () => MedusaContainer createScope: () => MedusaContainer
} }
export type ContainerLike = {
resolve<T = unknown>(key: string): T
}
@@ -0,0 +1,9 @@
import { ContainerLike } from "@medusajs/types"
export function createContainerLike(obj): ContainerLike {
return {
resolve(key: string) {
return obj[key]
},
}
}
+1
View File
@@ -2,6 +2,7 @@ export * from "./array-difference"
export * from "./build-query" export * from "./build-query"
export * from "./camel-to-snake-case" export * from "./camel-to-snake-case"
export * from "./container" export * from "./container"
export * from "./create-container-like"
export * from "./deduplicate" export * from "./deduplicate"
export * from "./deep-equal-obj" export * from "./deep-equal-obj"
export * from "./errors" export * from "./errors"