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:
Adrien de Peretti
2025-02-26 10:53:13 +01:00
committed by GitHub
parent a35c9ed741
commit eeebb35758
9 changed files with 277 additions and 130 deletions

View File

@@ -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 () => {
const entity1 = {
id: "1",
@@ -1106,7 +1196,9 @@ describe("mikroOrmRepository", () => {
describe("error mapping", () => {
it("should map UniqueConstraintViolationException to MedusaError on upsertWithReplace", async () => {
const entity3 = { title: "en3" }
await manager3().upsertWithReplace([entity3])
const err = await manager3()
.upsertWithReplace([entity3])
.catch((e) => e.message)

View File

@@ -803,15 +803,20 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
}
})
const qb = manager.qb(relation.pivotEntity)
await qb.insert(pivotData).onConflict().ignore().execute()
await manager.nativeDelete(relation.pivotEntity, {
[parentPivotColumn]: (data as any).id,
[currentPivotColumn]: {
$nin: pivotData.map((d) => d[currentPivotColumn]),
},
})
await promiseAll([
manager
.qb(relation.pivotEntity)
.insert(pivotData)
.onConflict()
.ignore()
.execute(),
manager.nativeDelete(relation.pivotEntity, {
[parentPivotColumn]: (data as any).id,
[currentPivotColumn]: {
$nin: pivotData.map((d) => d[currentPivotColumn]),
},
}),
])
return { entities: normalizedData, performedActions }
}
@@ -826,27 +831,23 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
joinColumnsConstraints[joinColumn] = data[referencedColumnName]
})
const toDeleteEntities = await manager.find<any, any, "id">(
relation.type,
{
...joinColumnsConstraints,
id: { $nin: normalizedData.map((d: any) => d.id) },
},
{
fields: ["id"],
}
const deletedRelations = await (
manager.getTransactionContext() ?? manager.getKnex()
)
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, {
...joinColumnsConstraints,
id: { $in: toDeleteIds },
})
if (toDeleteEntities.length) {
if (deletedRelations.length) {
performedActions.deleted[relation.type] ??= []
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: {},
}
await promiseAll(
entries.map(async (data) => {
const existingEntity = existingEntitiesMap.get(data.id)
orderedEntities.push(data)
if (existingEntity) {
if (skipUpdate) {
return
}
await manager.nativeUpdate(entityName, { id: data.id }, data)
performedActions.updated[entityName] ??= []
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 promises: Promise<any>[] = []
const toInsert: unknown[] = []
let shouldInsert = false
entries.map(async (data) => {
const existingEntity = existingEntitiesMap.get(data.id)
orderedEntities.push(data)
if (existingEntity) {
if (skipUpdate) {
return
}
})
)
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 }
}