fix(utils): Mikro orm repository update many to many should detach all items by default (#9917)

* fix(utils): Mikro orm repository update many to many should detach all items by default

* Create shiny-spiders-raise.md
This commit is contained in:
Adrien de Peretti
2024-11-05 08:53:55 +01:00
committed by GitHub
parent 61cb97da26
commit 16b4cc433e
3 changed files with 115 additions and 5 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---
fix(utils): Mikro orm repository update many to many should detach all items by default

View File

@@ -162,6 +162,42 @@ describe("mikroOrmRepository", () => {
await orm.close(true)
})
it("should successfully update a many to many collection providing an empty array", async () => {
const entity1 = {
id: "1",
title: "en1",
entity3: [{ title: "en3-1" }, { title: "en3-2" }],
}
let manager = orm.em.fork()
await manager1().create([entity1], { transactionManager: manager })
await manager.flush()
const [createdEntity1] = await manager1().find({
where: { id: "1" },
options: { populate: ["entity3"] },
})
expect(createdEntity1.entity3.getItems()).toHaveLength(2)
manager = orm.em.fork()
await manager1().update(
[{ entity: createdEntity1, update: { entity3: [] } }],
{
transactionManager: manager,
}
)
await manager.flush()
const updatedEntity1 = await manager1().find({
where: { id: "1" },
options: { populate: ["entity3"] },
})
expect(updatedEntity1).toHaveLength(1)
expect(updatedEntity1[0].entity3.getItems()).toHaveLength(0)
})
describe("upsert with replace", () => {
it("should successfully create a flat entity", async () => {
const entity1 = { id: "1", title: "en1", amount: 100 }

View File

@@ -330,15 +330,84 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
return entities
}
/**
* On a many to many relation, we expect to detach all the pivot items in case an empty array is provided.
* In that case, this relation needs to be init as well as its counter part in order to be
* able to perform the removal action.
*
* This action performs the initialization in the provided entity and therefore mutate in place.
*
* @param {{entity, update}[]} data
* @param context
* @private
*/
private async initManyToManyToDetachAllItemsIfNeeded(
data: { entity; update }[],
context?: Context
) {
const manager = this.getActiveManager<EntityManager>(context)
const relations = manager
.getDriver()
.getMetadata()
.get(entity.name).relations
// In case an empty array is provided for a collection relation of type m:n, this relation needs to be init in order to be
// able to perform an application cascade action.
const collectionsToRemoveAllFrom: Map<
string,
{ name: string; mappedBy?: string }
> = new Map()
data.forEach(({ update }) =>
Object.keys(update).filter((key) => {
const relation = relations.find((relation) => relation.name === key)
const shouldInit =
relation &&
relation.reference === ReferenceType.MANY_TO_MANY &&
Array.isArray(update[key]) &&
!update[key].length
if (shouldInit) {
collectionsToRemoveAllFrom.set(key, {
name: key,
mappedBy: relations.find((r) => r.name === key)?.mappedBy,
})
}
})
)
for (const [
collectionToRemoveAllFrom,
descriptor,
] of collectionsToRemoveAllFrom) {
await promiseAll(
data.map(async ({ entity }) => {
if (!descriptor.mappedBy) {
return await entity[collectionToRemoveAllFrom].init()
}
await entity[collectionToRemoveAllFrom].init()
const items = entity[collectionToRemoveAllFrom]
for (const item of items) {
await item[descriptor.mappedBy!].init()
}
})
)
}
}
async update(data: { entity; update }[], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const entities = data.map((data_) => {
return manager.assign(data_.entity, data_.update)
await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
data.map((_, index) => {
manager.assign(data[index].entity, data[index].update)
manager.persist(data[index].entity)
})
manager.persist(entities)
return entities
return data.map((d) => d.entity)
}
async delete(