chore(product): revamp upsertWithReplace and Remove its usage from product creation (#11585)
**What** - Move create product to use native create by structuring the data appropriately, it means no more `upsertWithReplace` being very poorly performant and got 20x better performances on staging - Improvements in `upsertWithReplace` to still get performance boost for places that still relies on it. Mostly bulking the operations when possible Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos R. L. Rodrigues
parent
a35c9ed741
commit
eeebb35758
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/product": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore(product): Remove upsertWithReplace where needed
|
||||||
@@ -533,7 +533,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail on non-existent product fields being present in the CSV", async () => {
|
it("should successfully skip non-existent product fields being present in the CSV", async () => {
|
||||||
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
const subscriberExecution = TestEventUtils.waitSubscribersExecution(
|
||||||
`${Modules.NOTIFICATION}.notification.${CommonEvents.CREATED}`,
|
`${Modules.NOTIFICATION}.notification.${CommonEvents.CREATED}`,
|
||||||
eventBus
|
eventBus
|
||||||
@@ -582,10 +582,21 @@ medusaIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
title: "Product import",
|
title: "Product import",
|
||||||
description: `Failed to import products from file test.csv`,
|
description:
|
||||||
|
"Product import of file test.csv completed successfully!",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [importedProduct] = (
|
||||||
|
await api.get("/admin/products?limit=1&order=-id", adminHeaders)
|
||||||
|
).data.products
|
||||||
|
|
||||||
|
expect(importedProduct).not.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
field: "Test product",
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("supports importing the v1 template", async () => {
|
it("supports importing the v1 template", async () => {
|
||||||
|
|||||||
+92
@@ -693,6 +693,96 @@ describe("mikroOrmRepository", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should successfully update, create, and delete subentities an entity with a one-to-many relation within a transaction", async () => {
|
||||||
|
const entity1 = {
|
||||||
|
id: "1",
|
||||||
|
title: "en1",
|
||||||
|
entity2: [
|
||||||
|
{ id: "2", title: "en2-1", handle: "some-handle" },
|
||||||
|
{ id: "3", title: "en2-2", handle: "some-other-handle" },
|
||||||
|
] as any[],
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entities: entities1, performedActions: performedActions1 } =
|
||||||
|
await manager1().transaction(async (txManager) => {
|
||||||
|
return await manager1().upsertWithReplace(
|
||||||
|
[entity1],
|
||||||
|
{
|
||||||
|
relations: ["entity2"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transactionManager: txManager,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(performedActions1).toEqual({
|
||||||
|
created: {
|
||||||
|
[Entity1.name]: [expect.objectContaining({ id: entity1.id })],
|
||||||
|
[Entity2.name]: entities1[0].entity2.map((entity2) =>
|
||||||
|
expect.objectContaining({ id: entity2.id })
|
||||||
|
),
|
||||||
|
},
|
||||||
|
updated: {},
|
||||||
|
deleted: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
entity1.entity2 = [
|
||||||
|
{ id: "2", title: "newen2-1" },
|
||||||
|
{ title: "en2-3", handle: "some-new-handle" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const { entities: entities2, performedActions: performedActions2 } =
|
||||||
|
await manager1().transaction(async (txManager) => {
|
||||||
|
return await manager1().upsertWithReplace(
|
||||||
|
[entity1],
|
||||||
|
{
|
||||||
|
relations: ["entity2"],
|
||||||
|
},
|
||||||
|
{ transactionManager: txManager }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const entity2En23 = entities2[0].entity2.find((e) => e.title === "en2-3")!
|
||||||
|
|
||||||
|
expect(performedActions2).toEqual({
|
||||||
|
created: {
|
||||||
|
[Entity2.name]: [expect.objectContaining({ id: entity2En23.id })],
|
||||||
|
},
|
||||||
|
updated: {
|
||||||
|
[Entity1.name]: [expect.objectContaining({ id: entity1.id })],
|
||||||
|
[Entity2.name]: [expect.objectContaining({ id: "2" })],
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
[Entity2.name]: [expect.objectContaining({ id: "3" })],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const listedEntities = await manager1().find({
|
||||||
|
where: { id: "1" },
|
||||||
|
options: { populate: ["entity2"] },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(listedEntities).toHaveLength(1)
|
||||||
|
expect(listedEntities[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "1",
|
||||||
|
title: "en1",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(listedEntities[0].entity2.getItems()).toHaveLength(2)
|
||||||
|
expect(listedEntities[0].entity2.getItems()).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "newen2-1",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "en2-3",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should update an entity with a one-to-many relation that has the same unique constraint key", async () => {
|
it("should update an entity with a one-to-many relation that has the same unique constraint key", async () => {
|
||||||
const entity1 = {
|
const entity1 = {
|
||||||
id: "1",
|
id: "1",
|
||||||
@@ -1106,7 +1196,9 @@ describe("mikroOrmRepository", () => {
|
|||||||
describe("error mapping", () => {
|
describe("error mapping", () => {
|
||||||
it("should map UniqueConstraintViolationException to MedusaError on upsertWithReplace", async () => {
|
it("should map UniqueConstraintViolationException to MedusaError on upsertWithReplace", async () => {
|
||||||
const entity3 = { title: "en3" }
|
const entity3 = { title: "en3" }
|
||||||
|
|
||||||
await manager3().upsertWithReplace([entity3])
|
await manager3().upsertWithReplace([entity3])
|
||||||
|
|
||||||
const err = await manager3()
|
const err = await manager3()
|
||||||
.upsertWithReplace([entity3])
|
.upsertWithReplace([entity3])
|
||||||
.catch((e) => e.message)
|
.catch((e) => e.message)
|
||||||
|
|||||||
@@ -803,15 +803,20 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const qb = manager.qb(relation.pivotEntity)
|
await promiseAll([
|
||||||
await qb.insert(pivotData).onConflict().ignore().execute()
|
manager
|
||||||
|
.qb(relation.pivotEntity)
|
||||||
await manager.nativeDelete(relation.pivotEntity, {
|
.insert(pivotData)
|
||||||
[parentPivotColumn]: (data as any).id,
|
.onConflict()
|
||||||
[currentPivotColumn]: {
|
.ignore()
|
||||||
$nin: pivotData.map((d) => d[currentPivotColumn]),
|
.execute(),
|
||||||
},
|
manager.nativeDelete(relation.pivotEntity, {
|
||||||
})
|
[parentPivotColumn]: (data as any).id,
|
||||||
|
[currentPivotColumn]: {
|
||||||
|
$nin: pivotData.map((d) => d[currentPivotColumn]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
return { entities: normalizedData, performedActions }
|
return { entities: normalizedData, performedActions }
|
||||||
}
|
}
|
||||||
@@ -826,27 +831,23 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
|
|||||||
joinColumnsConstraints[joinColumn] = data[referencedColumnName]
|
joinColumnsConstraints[joinColumn] = data[referencedColumnName]
|
||||||
})
|
})
|
||||||
|
|
||||||
const toDeleteEntities = await manager.find<any, any, "id">(
|
const deletedRelations = await (
|
||||||
relation.type,
|
manager.getTransactionContext() ?? manager.getKnex()
|
||||||
{
|
|
||||||
...joinColumnsConstraints,
|
|
||||||
id: { $nin: normalizedData.map((d: any) => d.id) },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ["id"],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
const toDeleteIds = toDeleteEntities.map((d: any) => d.id)
|
.queryBuilder()
|
||||||
|
.from(relation.targetMeta!.collection)
|
||||||
|
.delete()
|
||||||
|
.where(joinColumnsConstraints)
|
||||||
|
.whereNotIn(
|
||||||
|
"id",
|
||||||
|
normalizedData.map((d: any) => d.id)
|
||||||
|
)
|
||||||
|
.returning("id")
|
||||||
|
|
||||||
await manager.nativeDelete(relation.type, {
|
if (deletedRelations.length) {
|
||||||
...joinColumnsConstraints,
|
|
||||||
id: { $in: toDeleteIds },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (toDeleteEntities.length) {
|
|
||||||
performedActions.deleted[relation.type] ??= []
|
performedActions.deleted[relation.type] ??= []
|
||||||
performedActions.deleted[relation.type].push(
|
performedActions.deleted[relation.type].push(
|
||||||
...toDeleteEntities.map((d) => ({ id: d.id }))
|
...deletedRelations.map((row) => ({ id: row.id }))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,38 +971,46 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
|
|||||||
deleted: {},
|
deleted: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
await promiseAll(
|
const promises: Promise<any>[] = []
|
||||||
entries.map(async (data) => {
|
const toInsert: unknown[] = []
|
||||||
const existingEntity = existingEntitiesMap.get(data.id)
|
let shouldInsert = false
|
||||||
orderedEntities.push(data)
|
|
||||||
if (existingEntity) {
|
entries.map(async (data) => {
|
||||||
if (skipUpdate) {
|
const existingEntity = existingEntitiesMap.get(data.id)
|
||||||
return
|
orderedEntities.push(data)
|
||||||
}
|
if (existingEntity) {
|
||||||
await manager.nativeUpdate(entityName, { id: data.id }, data)
|
if (skipUpdate) {
|
||||||
performedActions.updated[entityName] ??= []
|
return
|
||||||
performedActions.updated[entityName].push({ id: data.id })
|
|
||||||
} else {
|
|
||||||
const qb = manager.qb(entityName)
|
|
||||||
if (skipUpdate) {
|
|
||||||
const res = await qb
|
|
||||||
.insert(data)
|
|
||||||
.onConflict()
|
|
||||||
.ignore()
|
|
||||||
.execute("all", true)
|
|
||||||
if (res) {
|
|
||||||
performedActions.created[entityName] ??= []
|
|
||||||
performedActions.created[entityName].push({ id: data.id })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await manager.insert(entityName, data)
|
|
||||||
performedActions.created[entityName] ??= []
|
|
||||||
performedActions.created[entityName].push({ id: data.id })
|
|
||||||
// await manager.insert(entityName, data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
const update = manager.nativeUpdate(entityName, { id: data.id }, data)
|
||||||
)
|
promises.push(update)
|
||||||
|
|
||||||
|
performedActions.updated[entityName] ??= []
|
||||||
|
performedActions.updated[entityName].push({ id: data.id })
|
||||||
|
} else {
|
||||||
|
shouldInsert = true
|
||||||
|
toInsert.push(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (shouldInsert) {
|
||||||
|
let insertQb = manager.qb(entityName).insert(toInsert).returning("id")
|
||||||
|
|
||||||
|
if (skipUpdate) {
|
||||||
|
insertQb = insertQb.onConflict().ignore()
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
insertQb.execute("all", true).then((res: { id: string }[]) => {
|
||||||
|
performedActions.created[entityName] ??= []
|
||||||
|
performedActions.created[entityName].push(
|
||||||
|
...res.map((data) => ({ id: data.id }))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await promiseAll(promises)
|
||||||
|
|
||||||
return { orderedEntities, performedActions }
|
return { orderedEntities, performedActions }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,14 +283,17 @@ export function defineHasManyRelationship(
|
|||||||
) {
|
) {
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
OneToMany({
|
const options: Parameters<typeof OneToMany>[0] = {
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
orphanRemoval: true,
|
orphanRemoval: true,
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
cascade: shouldRemoveRelated
|
}
|
||||||
? (["persist", "soft-remove"] as any)
|
|
||||||
: undefined,
|
if (shouldRemoveRelated) {
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
options.cascade = ["persist", "soft-remove"] as any
|
||||||
|
}
|
||||||
|
|
||||||
|
OneToMany(options)(MikroORMEntity.prototype, relationship.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -48,11 +48,11 @@ export const buildProductAndRelationsData = ({
|
|||||||
images,
|
images,
|
||||||
status,
|
status,
|
||||||
type_id,
|
type_id,
|
||||||
tags,
|
tag_ids,
|
||||||
options,
|
options,
|
||||||
variants,
|
variants,
|
||||||
collection_id,
|
collection_id,
|
||||||
}: Partial<ProductTypes.CreateProductDTO> & { tags: { value: string }[] }) => {
|
}: Partial<ProductTypes.CreateProductDTO>) => {
|
||||||
const defaultOptionTitle = "test-option"
|
const defaultOptionTitle = "test-option"
|
||||||
const defaultOptionValue = "test-value"
|
const defaultOptionValue = "test-value"
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const buildProductAndRelationsData = ({
|
|||||||
status: status ?? ProductStatus.PUBLISHED,
|
status: status ?? ProductStatus.PUBLISHED,
|
||||||
images: (images ?? []) as ProductImage[],
|
images: (images ?? []) as ProductImage[],
|
||||||
type_id,
|
type_id,
|
||||||
tags: tags ?? [{ value: "tag-1" }],
|
tag_ids,
|
||||||
collection_id,
|
collection_id,
|
||||||
options: options ?? [
|
options: options ?? [
|
||||||
{
|
{
|
||||||
|
|||||||
+11
-5
@@ -30,7 +30,7 @@ import {
|
|||||||
createTypes,
|
createTypes,
|
||||||
} from "../../__fixtures__/product"
|
} from "../../__fixtures__/product"
|
||||||
|
|
||||||
jest.setTimeout(300000)
|
jest.setTimeout(3000000)
|
||||||
|
|
||||||
moduleIntegrationTestRunner<IProductModuleService>({
|
moduleIntegrationTestRunner<IProductModuleService>({
|
||||||
moduleName: Modules.PRODUCT,
|
moduleName: Modules.PRODUCT,
|
||||||
@@ -181,6 +181,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should update a product and upsert relations that are not created yet", async () => {
|
it("should update a product and upsert relations that are not created yet", async () => {
|
||||||
|
const tags = await service.createProductTags([{ value: "tag-1" }])
|
||||||
const data = buildProductAndRelationsData({
|
const data = buildProductAndRelationsData({
|
||||||
images,
|
images,
|
||||||
thumbnail: images[0].url,
|
thumbnail: images[0].url,
|
||||||
@@ -190,6 +191,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
values: ["val-1", "val-2"],
|
values: ["val-1", "val-2"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
tag_ids: [tags[0].id],
|
||||||
})
|
})
|
||||||
|
|
||||||
const variantTitle = data.variants[0].title
|
const variantTitle = data.variants[0].title
|
||||||
@@ -217,7 +219,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
productBefore.options = data.options
|
productBefore.options = data.options
|
||||||
productBefore.images = data.images
|
productBefore.images = data.images
|
||||||
productBefore.thumbnail = data.thumbnail
|
productBefore.thumbnail = data.thumbnail
|
||||||
productBefore.tags = data.tags
|
productBefore.tag_ids = data.tag_ids
|
||||||
const updatedProducts = await service.upsertProducts([productBefore])
|
const updatedProducts = await service.upsertProducts([productBefore])
|
||||||
expect(updatedProducts).toHaveLength(1)
|
expect(updatedProducts).toHaveLength(1)
|
||||||
|
|
||||||
@@ -273,7 +275,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
tags: expect.arrayContaining([
|
tags: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
value: productBefore.tags?.[0].value,
|
value: tags[0].value,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
variants: expect.arrayContaining([
|
variants: expect.arrayContaining([
|
||||||
@@ -856,9 +858,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
describe("create", function () {
|
describe("create", function () {
|
||||||
let images = [{ url: "image-1" }]
|
let images = [{ url: "image-1" }]
|
||||||
it("should create a product", async () => {
|
it("should create a product", async () => {
|
||||||
|
const tags = await service.createProductTags([{ value: "tag-1" }])
|
||||||
const data = buildProductAndRelationsData({
|
const data = buildProductAndRelationsData({
|
||||||
images,
|
images,
|
||||||
thumbnail: images[0].url,
|
thumbnail: images[0].url,
|
||||||
|
tag_ids: [tags[0].id],
|
||||||
})
|
})
|
||||||
|
|
||||||
const productsCreated = await service.createProducts([data])
|
const productsCreated = await service.createProducts([data])
|
||||||
@@ -917,7 +921,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
tags: expect.arrayContaining([
|
tags: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
value: data.tags[0].value,
|
value: tags[0].value,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
variants: expect.arrayContaining([
|
variants: expect.arrayContaining([
|
||||||
@@ -1164,15 +1168,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
|
|||||||
productCollectionOne = collections[0]
|
productCollectionOne = collections[0]
|
||||||
productCollectionTwo = collections[1]
|
productCollectionTwo = collections[1]
|
||||||
|
|
||||||
|
const tags = await service.createProductTags([{ value: "tag-1" }])
|
||||||
|
|
||||||
const resp = await service.createProducts([
|
const resp = await service.createProducts([
|
||||||
buildProductAndRelationsData({
|
buildProductAndRelationsData({
|
||||||
collection_id: productCollectionOne.id,
|
collection_id: productCollectionOne.id,
|
||||||
options: [{ title: "size", values: ["large", "small"] }],
|
options: [{ title: "size", values: ["large", "small"] }],
|
||||||
variants: [{ title: "variant 1", options: { size: "small" } }],
|
variants: [{ title: "variant 1", options: { size: "small" } }],
|
||||||
|
tag_ids: [tags[0].id],
|
||||||
}),
|
}),
|
||||||
buildProductAndRelationsData({
|
buildProductAndRelationsData({
|
||||||
collection_id: productCollectionTwo.id,
|
collection_id: productCollectionTwo.id,
|
||||||
tags: [],
|
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
|
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
|
||||||
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
|
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
|
||||||
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
|
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
|
||||||
"test:integration": "jest --forceExit -- integration-tests/__tests__/**/*.ts",
|
"test:integration": "jest --runInBand --bail --forceExit -- integration-tests/__tests__/**/*.ts",
|
||||||
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
|
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
|
||||||
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
|
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
|
||||||
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
|
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { ProductCategoryService } from "@services"
|
|||||||
import {
|
import {
|
||||||
arrayDifference,
|
arrayDifference,
|
||||||
EmitEvents,
|
EmitEvents,
|
||||||
|
generateEntityId,
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
isDefined,
|
isDefined,
|
||||||
@@ -1568,75 +1569,82 @@ export default class ProductModuleService
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const { entities: productData } =
|
const tagIds = normalizedInput
|
||||||
await this.productService_.upsertWithReplace(
|
.flatMap((d) => (d as any).tags ?? [])
|
||||||
normalizedInput,
|
.map((t) => t.id)
|
||||||
|
let existingTags: InferEntityType<typeof ProductTag>[] = []
|
||||||
|
|
||||||
|
if (tagIds.length) {
|
||||||
|
existingTags = await this.productTagService_.list(
|
||||||
{
|
{
|
||||||
relations: ["tags", "categories"],
|
id: tagIds,
|
||||||
},
|
},
|
||||||
|
{},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await promiseAll(
|
const existingTagsMap = new Map(existingTags.map((tag) => [tag.id, tag]))
|
||||||
// Note: It's safe to rely on the order here as `upsertWithReplace` preserves the order of the input
|
|
||||||
normalizedInput.map(async (product, i) => {
|
|
||||||
const upsertedProduct: any = productData[i]
|
|
||||||
upsertedProduct.options = []
|
|
||||||
upsertedProduct.variants = []
|
|
||||||
|
|
||||||
if (product.options?.length) {
|
const productsToCreate = normalizedInput.map((product) => {
|
||||||
const { entities: productOptions } =
|
const productId = generateEntityId(product.id, "prod")
|
||||||
await this.productOptionService_.upsertWithReplace(
|
product.id = productId
|
||||||
product.options?.map((option) => ({
|
|
||||||
...option,
|
|
||||||
product_id: upsertedProduct.id,
|
|
||||||
})) ?? [],
|
|
||||||
{ relations: ["values"] },
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
upsertedProduct.options = productOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
if (product.variants?.length) {
|
if ((product as any).categories?.length) {
|
||||||
const { entities: productVariants } =
|
;(product as any).categories = (product as any).categories.map(
|
||||||
await this.productVariantService_.upsertWithReplace(
|
(category: { id: string }) => category.id
|
||||||
ProductModuleService.assignOptionsToVariants(
|
)
|
||||||
product.variants?.map((v) => ({
|
}
|
||||||
...v,
|
|
||||||
product_id: upsertedProduct.id,
|
|
||||||
})) ?? [],
|
|
||||||
upsertedProduct.options
|
|
||||||
),
|
|
||||||
{ relations: ["options"] },
|
|
||||||
sharedContext
|
|
||||||
)
|
|
||||||
upsertedProduct.variants = productVariants
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(product.images)) {
|
if (product.variants?.length) {
|
||||||
if (product.images.length) {
|
const normalizedVariants = product.variants.map((variant) => {
|
||||||
const { entities: productImages } =
|
const variantId = generateEntityId((variant as any).id, "variant")
|
||||||
await this.productImageService_.upsertWithReplace(
|
;(variant as any).id = variantId
|
||||||
product.images.map((image, rank) => ({
|
|
||||||
...image,
|
Object.entries(variant.options ?? {}).forEach(([key, value]) => {
|
||||||
product_id: upsertedProduct.id,
|
const productOption = product.options?.find(
|
||||||
rank,
|
(option) => option.title === key
|
||||||
})),
|
)!
|
||||||
{},
|
const productOptionValue = productOption.values?.find(
|
||||||
sharedContext
|
(optionValue) => (optionValue as any).value === value
|
||||||
)
|
)!
|
||||||
upsertedProduct.images = productImages
|
;(productOptionValue as any).variants ??= []
|
||||||
} else {
|
;(productOptionValue as any).variants.push(variant)
|
||||||
await this.productImageService_.delete(
|
})
|
||||||
{ product_id: upsertedProduct.id },
|
|
||||||
sharedContext
|
delete variant.options
|
||||||
|
|
||||||
|
return variant
|
||||||
|
})
|
||||||
|
|
||||||
|
product.variants = normalizedVariants
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((product as any).tags?.length) {
|
||||||
|
;(product as any).tags = (product as any).tags.map(
|
||||||
|
(tag: { id: string }) => {
|
||||||
|
const existingTag = existingTagsMap.get(tag.id)
|
||||||
|
if (existingTag) {
|
||||||
|
return existingTag
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Tag with id ${tag.id} not found. Please create the tag before associating it with the product.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
return product
|
||||||
|
})
|
||||||
|
|
||||||
|
const createdProducts = await this.productService_.create(
|
||||||
|
productsToCreate,
|
||||||
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
return productData
|
return createdProducts
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager()
|
@InjectTransactionManager()
|
||||||
@@ -1916,6 +1924,17 @@ export default class ProductModuleService
|
|||||||
productData.thumbnail = productData.images[0].url
|
productData.thumbnail = productData.images[0].url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (productData.images?.length) {
|
||||||
|
productData.images = productData.images.map((image, index) =>
|
||||||
|
(image as { rank?: number }).rank != null
|
||||||
|
? image
|
||||||
|
: {
|
||||||
|
...image,
|
||||||
|
rank: index,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return productData
|
return productData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1929,6 +1948,7 @@ export default class ProductModuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (productData.options?.length) {
|
if (productData.options?.length) {
|
||||||
|
// TODO: Instead of fetching per product, this should fetch for all product allowing for only one query instead of X
|
||||||
const dbOptions = await this.productOptionService_.list(
|
const dbOptions = await this.productOptionService_.list(
|
||||||
{ product_id: productData.id },
|
{ product_id: productData.id },
|
||||||
{ relations: ["values"] },
|
{ relations: ["values"] },
|
||||||
|
|||||||
Reference in New Issue
Block a user