From e84d0fd2da85fe94ca78c47eaf93339bfe51cf88 Mon Sep 17 00:00:00 2001 From: adrien2p Date: Mon, 4 Nov 2024 11:59:55 +0100 Subject: [PATCH 1/2] fix(utils): Mikro orm repository update many to many should detach all items by default --- .../__tests__/mikro-orm-repository.spec.ts | 36 +++++++++ .../src/dal/mikro-orm/mikro-orm-repository.ts | 79 +++++++++++++++++-- 2 files changed, 110 insertions(+), 5 deletions(-) diff --git a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts index 060de818dfa78..8ffef62387006 100644 --- a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts +++ b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts @@ -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 } diff --git a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 6b7e03da32199..16939238fcda8 100644 --- a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -330,15 +330,84 @@ export function mikroOrmBaseRepositoryFactory( 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(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 { const manager = this.getActiveManager(context) - const entities = data.map((data_) => { - return manager.assign(data_.entity, data_.update) - }) - manager.persist(entities) + await this.initManyToManyToDetachAllItemsIfNeeded(data, context) - return entities + data.map((_, index) => { + manager.assign(data[index].entity, data[index].update) + manager.persist(data[index].entity) + }) + + return data.map((d) => d.entity) } async delete( From e1d10013b0810b4ab42a1df8c4c717af31e23862 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 4 Nov 2024 14:23:54 +0100 Subject: [PATCH 2/2] Create shiny-spiders-raise.md --- .changeset/shiny-spiders-raise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-spiders-raise.md diff --git a/.changeset/shiny-spiders-raise.md b/.changeset/shiny-spiders-raise.md new file mode 100644 index 0000000000000..fc1e30731c55d --- /dev/null +++ b/.changeset/shiny-spiders-raise.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +fix(utils): Mikro orm repository update many to many should detach all items by default