chore(util): Detect circular dependencies on soft delete (#6489)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore(util): Detect circular dependencies on soft delete
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import {
|
||||||
|
Collection,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
|
||||||
|
// Circular dependency one level
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class RecursiveEntity1 {
|
||||||
|
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@OneToMany(() => RecursiveEntity2, (entity2) => entity2.entity1, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity2 = new Collection<RecursiveEntity2>(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class RecursiveEntity2 {
|
||||||
|
constructor(props: {
|
||||||
|
id: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
entity1: RecursiveEntity1
|
||||||
|
}) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
this.entity1 = props.entity1
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => RecursiveEntity1, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity1: RecursiveEntity1
|
||||||
|
}
|
||||||
|
|
||||||
|
// No circular dependency
|
||||||
|
@Entity()
|
||||||
|
class Entity1 {
|
||||||
|
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@OneToMany(() => Entity2, (entity2) => entity2.entity1, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity2 = new Collection<Entity2>(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class Entity2 {
|
||||||
|
constructor(props: {
|
||||||
|
id: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
entity1: Entity1
|
||||||
|
}) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
this.entity1 = props.entity1
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => Entity1)
|
||||||
|
entity1: Entity1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circular dependency deep level
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class DeepRecursiveEntity1 {
|
||||||
|
constructor(props: { id: string; deleted_at: Date | null }) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@OneToMany(() => DeepRecursiveEntity2, (entity2) => entity2.entity1, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity2 = new Collection<DeepRecursiveEntity2>(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class DeepRecursiveEntity2 {
|
||||||
|
constructor(props: {
|
||||||
|
id: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
entity3: DeepRecursiveEntity3
|
||||||
|
}) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
this.entity3 = props.entity3
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => DeepRecursiveEntity1)
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
|
||||||
|
@ManyToOne(() => DeepRecursiveEntity3, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity3: DeepRecursiveEntity3
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class DeepRecursiveEntity3 {
|
||||||
|
constructor(props: {
|
||||||
|
id: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
}) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
this.entity1 = props.entity1
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => DeepRecursiveEntity1, {
|
||||||
|
cascade: ["soft-remove"] as any,
|
||||||
|
})
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
class DeepRecursiveEntity4 {
|
||||||
|
constructor(props: {
|
||||||
|
id: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
}) {
|
||||||
|
this.id = props.id
|
||||||
|
this.deleted_at = props.deleted_at
|
||||||
|
this.entity1 = props.entity1
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
deleted_at: Date | null
|
||||||
|
|
||||||
|
@ManyToOne(() => DeepRecursiveEntity1)
|
||||||
|
entity1: DeepRecursiveEntity1
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
RecursiveEntity1,
|
||||||
|
RecursiveEntity2,
|
||||||
|
Entity1,
|
||||||
|
Entity2,
|
||||||
|
DeepRecursiveEntity1,
|
||||||
|
DeepRecursiveEntity2,
|
||||||
|
DeepRecursiveEntity3,
|
||||||
|
DeepRecursiveEntity4,
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { mikroOrmUpdateDeletedAtRecursively } from "../utils"
|
||||||
|
import { MikroORM } from "@mikro-orm/core"
|
||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import {
|
||||||
|
DeepRecursiveEntity1,
|
||||||
|
DeepRecursiveEntity2,
|
||||||
|
DeepRecursiveEntity3,
|
||||||
|
DeepRecursiveEntity4,
|
||||||
|
Entity1,
|
||||||
|
Entity2,
|
||||||
|
RecursiveEntity1,
|
||||||
|
RecursiveEntity2,
|
||||||
|
} from "../__fixtures__/utils"
|
||||||
|
|
||||||
|
jest.mock("@mikro-orm/core", () => ({
|
||||||
|
...jest.requireActual("@mikro-orm/core"),
|
||||||
|
wrap: jest.fn().mockImplementation((entity) => ({
|
||||||
|
...entity,
|
||||||
|
init: jest.fn().mockResolvedValue(entity),
|
||||||
|
__helper: {
|
||||||
|
isInitialized: jest.fn().mockReturnValue(true),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe("mikroOrmUpdateDeletedAtRecursively", () => {
|
||||||
|
describe("using circular cascading", () => {
|
||||||
|
let orm!: MikroORM
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
orm = await MikroORM.init({
|
||||||
|
entities: [
|
||||||
|
Entity1,
|
||||||
|
Entity2,
|
||||||
|
RecursiveEntity1,
|
||||||
|
RecursiveEntity2,
|
||||||
|
DeepRecursiveEntity1,
|
||||||
|
DeepRecursiveEntity2,
|
||||||
|
DeepRecursiveEntity3,
|
||||||
|
DeepRecursiveEntity4,
|
||||||
|
],
|
||||||
|
dbName: "test",
|
||||||
|
type: "postgresql",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await orm.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should successfully mark the entities deleted_at recursively", async () => {
|
||||||
|
const manager = orm.em.fork() as SqlEntityManager
|
||||||
|
const entity1 = new Entity1({ id: "1", deleted_at: null })
|
||||||
|
const entity2 = new Entity2({
|
||||||
|
id: "2",
|
||||||
|
deleted_at: null,
|
||||||
|
entity1: entity1,
|
||||||
|
})
|
||||||
|
entity1.entity2.add(entity2)
|
||||||
|
|
||||||
|
const deletedAt = new Date()
|
||||||
|
await mikroOrmUpdateDeletedAtRecursively(manager, [entity1], deletedAt)
|
||||||
|
|
||||||
|
expect(entity1.deleted_at).toEqual(deletedAt)
|
||||||
|
expect(entity2.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 })
|
||||||
|
const entity2 = new RecursiveEntity2({
|
||||||
|
id: "2",
|
||||||
|
deleted_at: null,
|
||||||
|
entity1: entity1,
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
mikroOrmUpdateDeletedAtRecursively(manager, [entity2], new Date())
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Unable to soft delete the entity1. Circular dependency detected: RecursiveEntity2 -> entity1 -> RecursiveEntity1 -> entity2 -> RecursiveEntity2"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw an error when a circular dependency is detected even at a deeper level", async () => {
|
||||||
|
const manager = orm.em.fork() as SqlEntityManager
|
||||||
|
const entity1 = new DeepRecursiveEntity1({ id: "1", deleted_at: null })
|
||||||
|
const entity3 = new DeepRecursiveEntity3({
|
||||||
|
id: "3",
|
||||||
|
deleted_at: null,
|
||||||
|
entity1: entity1,
|
||||||
|
})
|
||||||
|
const entity2 = new DeepRecursiveEntity2({
|
||||||
|
id: "2",
|
||||||
|
deleted_at: null,
|
||||||
|
entity1: entity1,
|
||||||
|
entity3: entity3,
|
||||||
|
})
|
||||||
|
const entity4 = new DeepRecursiveEntity4({
|
||||||
|
id: "4",
|
||||||
|
deleted_at: null,
|
||||||
|
entity1: entity1,
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
mikroOrmUpdateDeletedAtRecursively(manager, [entity1], new Date())
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Unable to soft delete the entity2. Circular dependency detected: DeepRecursiveEntity1 -> entity2 -> DeepRecursiveEntity2 -> entity3 -> DeepRecursiveEntity3 -> entity1 -> DeepRecursiveEntity1"
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
mikroOrmUpdateDeletedAtRecursively(manager, [entity2], new Date())
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Unable to soft delete the entity3. Circular dependency detected: DeepRecursiveEntity2 -> entity3 -> DeepRecursiveEntity3 -> entity1 -> DeepRecursiveEntity1 -> entity2 -> DeepRecursiveEntity2"
|
||||||
|
)
|
||||||
|
|
||||||
|
await mikroOrmUpdateDeletedAtRecursively(manager, [entity4], new Date())
|
||||||
|
expect(entity4.deleted_at).not.toBeNull()
|
||||||
|
expect(entity1.deleted_at).toBeNull()
|
||||||
|
expect(entity2.deleted_at).toBeNull()
|
||||||
|
expect(entity3.deleted_at).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
transactionWrapper,
|
transactionWrapper,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
import { mikroOrmSerializer, mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
import { mikroOrmSerializer, mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
|
||||||
export class MikroOrmBase<T = any> {
|
export class MikroOrmBase<T = any> {
|
||||||
readonly manager_: any
|
readonly manager_: any
|
||||||
@@ -137,7 +138,7 @@ export class MikroOrmBaseRepository<T extends object = object>
|
|||||||
const entities = await this.find({ where: filter as any }, sharedContext)
|
const entities = await this.find({ where: filter as any }, sharedContext)
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
const manager = this.getActiveManager(sharedContext)
|
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||||
await mikroOrmUpdateDeletedAtRecursively<T>(
|
await mikroOrmUpdateDeletedAtRecursively<T>(
|
||||||
manager,
|
manager,
|
||||||
entities as any[],
|
entities as any[],
|
||||||
@@ -173,7 +174,7 @@ export class MikroOrmBaseRepository<T extends object = object>
|
|||||||
|
|
||||||
const entities = await this.find(query, sharedContext)
|
const entities = await this.find(query, sharedContext)
|
||||||
|
|
||||||
const manager = this.getActiveManager(sharedContext)
|
const manager = this.getActiveManager<SqlEntityManager>(sharedContext)
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null)
|
await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null)
|
||||||
|
|
||||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||||
|
|||||||
@@ -1,74 +1,123 @@
|
|||||||
import { buildQuery } from "../../modules-sdk"
|
import { buildQuery } from "../../modules-sdk"
|
||||||
|
import { EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
|
||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
|
||||||
|
function detectCircularDependency(
|
||||||
|
manager: SqlEntityManager,
|
||||||
|
entityMetadata: EntityMetadata,
|
||||||
|
visited: Set<string> = new Set()
|
||||||
|
) {
|
||||||
|
visited.add(entityMetadata.className)
|
||||||
|
|
||||||
|
const relations = entityMetadata.relations
|
||||||
|
const relationsToCascade = relations.filter((relation) =>
|
||||||
|
relation.cascade.includes("soft-remove" as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const relation of relationsToCascade) {
|
||||||
|
const branchVisited = new Set(Array.from(visited))
|
||||||
|
|
||||||
|
if (branchVisited.has(relation.name)) {
|
||||||
|
const dependencies = Array.from(visited)
|
||||||
|
dependencies.push(entityMetadata.className)
|
||||||
|
const circularDependencyStr = dependencies.join(" -> ")
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unable to soft delete the ${relation.name}. Circular dependency detected: ${circularDependencyStr}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
branchVisited.add(relation.name)
|
||||||
|
|
||||||
|
const relationEntityMetadata = manager
|
||||||
|
.getDriver()
|
||||||
|
.getMetadata()
|
||||||
|
.get(relation.type)
|
||||||
|
|
||||||
|
detectCircularDependency(manager, relationEntityMetadata, branchVisited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performCascadingSoftDeletion<T>(
|
||||||
|
manager: SqlEntityManager,
|
||||||
|
entity: T & { id: string; deleted_at?: string | Date | null },
|
||||||
|
value: Date | null
|
||||||
|
) {
|
||||||
|
if (!("deleted_at" in entity)) return
|
||||||
|
|
||||||
|
entity.deleted_at = value
|
||||||
|
|
||||||
|
const entityName = entity.constructor.name
|
||||||
|
|
||||||
|
const relations = manager.getDriver().getMetadata().get(entityName).relations
|
||||||
|
|
||||||
|
const relationsToCascade = relations.filter((relation) =>
|
||||||
|
relation.cascade.includes("soft-remove" as any)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const relation of relationsToCascade) {
|
||||||
|
let entityRelation = entity[relation.name]
|
||||||
|
|
||||||
|
// Handle optional relationships
|
||||||
|
if (relation.nullable && !entityRelation) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrieveEntity = async () => {
|
||||||
|
const query = buildQuery(
|
||||||
|
{
|
||||||
|
id: entity.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relations: [relation.name],
|
||||||
|
withDeleted: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await manager.findOne(
|
||||||
|
entity.constructor.name,
|
||||||
|
query.where,
|
||||||
|
query.options as FindOptions<any>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entityRelation) {
|
||||||
|
// Fixes the case of many to many through pivot table
|
||||||
|
entityRelation = await retrieveEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCollection = "toArray" in entityRelation
|
||||||
|
let relationEntities: any[] = []
|
||||||
|
|
||||||
|
if (isCollection) {
|
||||||
|
if (!entityRelation.isInitialized()) {
|
||||||
|
entityRelation = await retrieveEntity()
|
||||||
|
entityRelation = entityRelation[relation.name]
|
||||||
|
}
|
||||||
|
relationEntities = entityRelation.getItems()
|
||||||
|
} else {
|
||||||
|
const initializedEntityRelation = await wrap(entityRelation).init()
|
||||||
|
relationEntities = [initializedEntityRelation]
|
||||||
|
}
|
||||||
|
|
||||||
|
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.persist(entity)
|
||||||
|
}
|
||||||
|
|
||||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||||
T extends object = any
|
T extends object = any
|
||||||
>(
|
>(
|
||||||
manager: any,
|
manager: SqlEntityManager,
|
||||||
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
||||||
value: Date | null
|
value: Date | null
|
||||||
) => {
|
) => {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
if (!("deleted_at" in entity)) continue
|
const entityMetadata = manager
|
||||||
|
|
||||||
entity.deleted_at = value
|
|
||||||
|
|
||||||
const relations = manager
|
|
||||||
.getDriver()
|
.getDriver()
|
||||||
.getMetadata()
|
.getMetadata()
|
||||||
.get(entity.constructor.name).relations
|
.get(entity.constructor.name)
|
||||||
|
detectCircularDependency(manager, entityMetadata)
|
||||||
const relationsToCascade = relations.filter((relation) =>
|
await performCascadingSoftDeletion(manager, entity, value)
|
||||||
relation.cascade.includes("soft-remove" as any)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const relation of relationsToCascade) {
|
|
||||||
let entityRelation = entity[relation.name]
|
|
||||||
|
|
||||||
// Handle optional relationships
|
|
||||||
if (relation.nullable && !entityRelation) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const retrieveEntity = async () => {
|
|
||||||
const query = buildQuery(
|
|
||||||
{
|
|
||||||
id: entity.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
relations: [relation.name],
|
|
||||||
withDeleted: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return await manager.findOne(
|
|
||||||
entity.constructor.name,
|
|
||||||
query.where,
|
|
||||||
query.options
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entityRelation) {
|
|
||||||
// Fixes the case of many to many through pivot table
|
|
||||||
entityRelation = await retrieveEntity()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCollection = "toArray" in entityRelation
|
|
||||||
let relationEntities: any[] = []
|
|
||||||
|
|
||||||
if (isCollection) {
|
|
||||||
if (!entityRelation.isInitialized()) {
|
|
||||||
entityRelation = await retrieveEntity()
|
|
||||||
entityRelation = entityRelation[relation.name]
|
|
||||||
}
|
|
||||||
relationEntities = entityRelation.getItems()
|
|
||||||
} else {
|
|
||||||
const initializedEntityRelation = await entityRelation.__helper?.init()
|
|
||||||
relationEntities = [initializedEntityRelation]
|
|
||||||
}
|
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.persist(entity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user