diff --git a/.changeset/fair-baboons-cry.md b/.changeset/fair-baboons-cry.md new file mode 100644 index 0000000000..1e8e45a973 --- /dev/null +++ b/.changeset/fair-baboons-cry.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +chore(utils): Soft delete should allow self referencing circular deps diff --git a/packages/tax/integration-tests/__tests__/index.spec.ts b/packages/tax/integration-tests/__tests__/index.spec.ts index 261f946eb4..753f9e2c4d 100644 --- a/packages/tax/integration-tests/__tests__/index.spec.ts +++ b/packages/tax/integration-tests/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import { ITaxModuleService } from "@medusajs/types" import { Modules } from "@medusajs/modules-sdk" diff --git a/packages/utils/src/dal/mikro-orm/__fixtures__/utils.ts b/packages/utils/src/dal/mikro-orm/__fixtures__/utils.ts index c4f28a95f6..5065c13a5e 100644 --- a/packages/utils/src/dal/mikro-orm/__fixtures__/utils.ts +++ b/packages/utils/src/dal/mikro-orm/__fixtures__/utils.ts @@ -189,6 +189,42 @@ class DeepRecursiveEntity4 { entity1: DeepRecursiveEntity1 } +// Internal circular dependency + +@Entity() +class InternalCircularDependencyEntity1 { + constructor(props: { + id: string + deleted_at: Date | null + parent?: InternalCircularDependencyEntity1 + }) { + this.id = props.id + this.deleted_at = props.deleted_at + + if (props.parent) { + this.parent = props.parent + } + } + + @PrimaryKey() + id: string + + @Property() + deleted_at: Date | null + + @OneToMany( + () => InternalCircularDependencyEntity1, + (entity) => entity.parent, + { + cascade: ["soft-remove"] as any, + } + ) + children = new Collection(this) + + @ManyToOne(() => InternalCircularDependencyEntity1) + parent: InternalCircularDependencyEntity1 +} + export { RecursiveEntity1, RecursiveEntity2, @@ -198,4 +234,5 @@ export { DeepRecursiveEntity2, DeepRecursiveEntity3, DeepRecursiveEntity4, + InternalCircularDependencyEntity1, } diff --git a/packages/utils/src/dal/mikro-orm/__tests__/utils.spec.ts b/packages/utils/src/dal/mikro-orm/__tests__/utils.spec.ts index b1e14f63a5..5b8995771c 100644 --- a/packages/utils/src/dal/mikro-orm/__tests__/utils.spec.ts +++ b/packages/utils/src/dal/mikro-orm/__tests__/utils.spec.ts @@ -8,6 +8,7 @@ import { DeepRecursiveEntity4, Entity1, Entity2, + InternalCircularDependencyEntity1, RecursiveEntity1, RecursiveEntity2, } from "../__fixtures__/utils" @@ -38,6 +39,7 @@ describe("mikroOrmUpdateDeletedAtRecursively", () => { DeepRecursiveEntity2, DeepRecursiveEntity3, DeepRecursiveEntity4, + InternalCircularDependencyEntity1, ], dbName: "test", type: "postgresql", @@ -65,6 +67,26 @@ describe("mikroOrmUpdateDeletedAtRecursively", () => { expect(entity2.deleted_at).toEqual(deletedAt) }) + it("should successfully mark the entities deleted_at recursively with internal parent/child relation", async () => { + const manager = orm.em.fork() as SqlEntityManager + const entity1 = new InternalCircularDependencyEntity1({ + id: "1", + deleted_at: null, + }) + + const childEntity1 = new InternalCircularDependencyEntity1({ + id: "2", + deleted_at: null, + parent: entity1, + }) + + const deletedAt = new Date() + await mikroOrmUpdateDeletedAtRecursively(manager, [entity1], deletedAt) + + expect(entity1.deleted_at).toEqual(deletedAt) + expect(childEntity1.deleted_at).toEqual(deletedAt) + }) + it("should throw an error when a circular dependency is detected", async () => { const manager = orm.em.fork() as SqlEntityManager const entity1 = new RecursiveEntity1({ id: "1", deleted_at: null }) diff --git a/packages/utils/src/dal/mikro-orm/utils.ts b/packages/utils/src/dal/mikro-orm/utils.ts index 4596bdcb85..a80ff2286c 100644 --- a/packages/utils/src/dal/mikro-orm/utils.ts +++ b/packages/utils/src/dal/mikro-orm/utils.ts @@ -5,8 +5,13 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" function detectCircularDependency( manager: SqlEntityManager, entityMetadata: EntityMetadata, - visited: Set = new Set() + visited: Set = new Set(), + shouldStop: boolean = false ) { + if (shouldStop) { + return + } + visited.add(entityMetadata.className) const relations = entityMetadata.relations @@ -17,7 +22,9 @@ function detectCircularDependency( for (const relation of relationsToCascade) { const branchVisited = new Set(Array.from(visited)) - if (branchVisited.has(relation.name)) { + const isSelfCircularDependency = entityMetadata.class === relation.entity() + + if (!isSelfCircularDependency && branchVisited.has(relation.name)) { const dependencies = Array.from(visited) dependencies.push(entityMetadata.className) const circularDependencyStr = dependencies.join(" -> ") @@ -33,7 +40,12 @@ function detectCircularDependency( .getMetadata() .get(relation.type) - detectCircularDependency(manager, relationEntityMetadata, branchVisited) + detectCircularDependency( + manager, + relationEntityMetadata, + branchVisited, + isSelfCircularDependency + ) } } @@ -82,6 +94,9 @@ async function performCascadingSoftDeletion( if (!entityRelation) { // Fixes the case of many to many through pivot table entityRelation = await retrieveEntity() + if (!entityRelation) { + continue + } } const isCollection = "toArray" in entityRelation @@ -94,10 +109,17 @@ async function performCascadingSoftDeletion( } relationEntities = entityRelation.getItems() } else { - const initializedEntityRelation = await wrap(entityRelation).init() + const wrappedEntity = wrap(entityRelation) + const initializedEntityRelation = wrappedEntity.isInitialized() + ? entityRelation + : await wrap(entityRelation).init() relationEntities = [initializedEntityRelation] } + if (!relationEntities.length) { + continue + } + await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value) }