Skip to content

Commit

Permalink
chore(): Improve cascade soft deletetion and update
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien2p committed Feb 26, 2025
1 parent cdfa21c commit 5c1b6b0
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 60 deletions.
17 changes: 10 additions & 7 deletions packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,38 +398,41 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
descriptor,
] of collectionsToRemoveAllFrom) {
await promiseAll(
data.map(async ({ entity }) => {
data.flatMap(async ({ entity }) => {
if (!descriptor.mappedBy) {
return await entity[collectionToRemoveAllFrom].init()
}

const promises: Promise<any>[] = []
await entity[collectionToRemoveAllFrom].init()
const items = entity[collectionToRemoveAllFrom]

for (const item of items) {
await item[descriptor.mappedBy!].init()
promises.push(item[descriptor.mappedBy!].init())
}

return promises
})
)
}
}

async update(
data: { entity; update }[],
data: { entity: any; update: any }[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context)

await this.initManyToManyToDetachAllItemsIfNeeded(data, context)

data.map((_, index) => {
manager.assign(data[index].entity, data[index].update, {
data.forEach(({ entity, update }) => {
manager.assign(entity, update, {
mergeObjectProperties: true,
})
manager.persist(data[index].entity)
manager.persist(entity)
})

return data.map((d) => d.entity)
return data.map((d) => d.entity) as InferRepositoryReturnType<T>[]
}

async delete(
Expand Down
108 changes: 62 additions & 46 deletions packages/core/utils/src/dal/mikro-orm/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Collection, EntityMetadata, FindOptions, wrap } from "@mikro-orm/core"
import { EntityMetadata, FindOptions } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { buildQuery } from "../../modules-sdk/build-query"
import { isString } from "../../common/is-string"
import { buildQuery } from "../../modules-sdk/build-query"

function detectCircularDependency(
manager: SqlEntityManager,
Expand Down Expand Up @@ -67,40 +67,51 @@ async function performCascadingSoftDeletion<T>(
entity.deleted_at = value

const entityName = entity.constructor.name

const relations = manager.getDriver().getMetadata().get(entityName).relations
const entityMetadata = manager.getDriver().getMetadata().get(entityName)
const relations = entityMetadata.relations

const relationsToCascade = relations.filter((relation) =>
relation.cascade?.includes("soft-remove" as any)
)

for (const relation of relationsToCascade) {
let entityRelation = entity[relation.name]
// If there are no relations to cascade, just persist the entity and return
if (!relationsToCascade.length) {
manager.persist(entity)
return
}

// Handle optional relationships
if (relation.nullable && !entityRelation) {
continue
}
// Fetch the entity with all cascading relations in a single query
const relationNames = relationsToCascade.map((r) => r.name)

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>
)
const query = buildQuery(
{
id: entity.id,
},
{
relations: relationNames,
withDeleted: true,
}
)

const entityWithRelations = await manager.findOne(
entityName,
query.where,
query.options as FindOptions<any>
)

if (!entityWithRelations) {
manager.persist(entity)
return
}

// Create a map to group related entities by their type
const relatedEntitiesByType = new Map<string, any[]>()

// Collect all related entities by type
for (const relation of relationsToCascade) {
const entityRelation = entityWithRelations[relation.name]

entityRelation = await retrieveEntity()
entityRelation = entityRelation[relation.name]
// Skip if relation is null or undefined
if (!entityRelation) {
continue
}
Expand All @@ -109,30 +120,31 @@ async function performCascadingSoftDeletion<T>(
let relationEntities: any[] = []

if (isCollection) {
if (!(entityRelation as Collection<any, any>).isInitialized()) {
entityRelation = await retrieveEntity()
entityRelation = entityRelation[relation.name]
}
relationEntities = entityRelation.getItems()
} else {
const wrappedEntity = wrap(entityRelation)

let initializedEntityRelation = entityRelation
if (!wrappedEntity.isInitialized()) {
initializedEntityRelation = await wrap(entityRelation).init()
}

relationEntities = [initializedEntityRelation]
relationEntities = [entityRelation]
}

if (!relationEntities.length) {
continue
}

await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
// Add to the map of entities by type
if (!relatedEntitiesByType.has(relation.type)) {
relatedEntitiesByType.set(relation.type, [])
}
relatedEntitiesByType.get(relation.type)!.push(...relationEntities)
}

await manager.persist(entity)
// Process each type of related entity in batch
for (const [, entities] of relatedEntitiesByType.entries()) {
if (entities.length === 0) continue

// Process cascading relations for these entities
await mikroOrmUpdateDeletedAtRecursively(manager, entities, value)
}

manager.persist(entity)
}

export const mikroOrmUpdateDeletedAtRecursively = async <
Expand All @@ -142,12 +154,16 @@ export const mikroOrmUpdateDeletedAtRecursively = async <
entities: (T & { id: string; deleted_at?: string | Date | null })[],
value: Date | null
) => {
if (!entities.length) return

const entityMetadata = manager
.getDriver()
.getMetadata()
.get(entities[0].constructor.name)
detectCircularDependency(manager, entityMetadata)

// Process each entity type
for (const entity of entities) {
const entityMetadata = manager
.getDriver()
.getMetadata()
.get(entity.constructor.name)
detectCircularDependency(manager, entityMetadata)
await performCascadingSoftDeletion(manager, entity, value)
}
}
17 changes: 12 additions & 5 deletions packages/core/utils/src/modules-sdk/medusa-internal-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export function MedusaInternalService<
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
const inputArray = Array.isArray(input) ? input : [input]

const toUpdateData: { entity; update }[] = []
const toUpdateData: { entity: TEntity; update: Partial<TEntity> }[] = []

// Only used when we receive data and no selector
const keySelectorForDataOnly: any = {
Expand Down Expand Up @@ -353,10 +353,17 @@ export function MedusaInternalService<

// Manage metadata if needed
toUpdateData.forEach(({ entity, update }) => {
if (isPresent(update.metadata)) {
entity.metadata = update.metadata = mergeMetadata(
entity.metadata ?? {},
update.metadata
const update_ = update as (typeof toUpdateData)[number]["update"] & {
metadata: Record<string, unknown>
}
const entity_ = entity as InferEntityType<TEntity> & {
metadata?: Record<string, unknown>
}

if (isPresent(update_.metadata)) {
entity_.metadata = update_.metadata = mergeMetadata(
entity_.metadata ?? {},
update_.metadata
)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const buildProductAndRelationsData = ({
options,
variants,
collection_id,
}: Partial<ProductTypes.CreateProductDTO>) => {
}: Partial<ProductTypes.CreateProductDTO> & { tags?: { value: string }[] }) => {
const defaultOptionTitle = "test-option"
const defaultOptionValue = "test-value"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
injectedDependencies: {
[Modules.EVENT_BUS]: new MockEventBusService(),
},
debug: true,
testSuite: ({ MikroOrmWrapper, service }) => {
describe("ProductModuleService products", function () {
let productCollectionOne: ProductCollection
Expand Down Expand Up @@ -969,12 +970,91 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
})

describe("softDelete", function () {
describe.only("softDelete", function () {
let images = [{ url: "image-1" }]
it("should soft delete a product and its cascaded relations", async () => {
const data = buildProductAndRelationsData({
images,
thumbnail: images[0].url,
options: [
{ title: "size", values: ["large", "small"] },
{ title: "color", values: ["red", "blue"] },
{ title: "material", values: ["cotton", "polyester"] },
],
variants: [
{
title: "Large Red Cotton",
sku: "LRG-RED-CTN",
options: {
size: "large",
color: "red",
material: "cotton",
},
},
{
title: "Large Red Polyester",
sku: "LRG-RED-PLY",
options: {
size: "large",
color: "red",
material: "polyester",
},
},
{
title: "Large Blue Cotton",
sku: "LRG-BLU-CTN",
options: {
size: "large",
color: "blue",
material: "cotton",
},
},
{
title: "Large Blue Polyester",
sku: "LRG-BLU-PLY",
options: {
size: "large",
color: "blue",
material: "polyester",
},
},
{
title: "Small Red Cotton",
sku: "SML-RED-CTN",
options: {
size: "small",
color: "red",
material: "cotton",
},
},
{
title: "Small Red Polyester",
sku: "SML-RED-PLY",
options: {
size: "small",
color: "red",
material: "polyester",
},
},
{
title: "Small Blue Cotton",
sku: "SML-BLU-CTN",
options: {
size: "small",
color: "blue",
material: "cotton",
},
},
{
title: "Small Blue Polyester",
sku: "SML-BLU-PLY",
options: {
size: "small",
color: "blue",
material: "polyester",
},
},
],
})

const products = await service.createProducts([data])
Expand Down

0 comments on commit 5c1b6b0

Please sign in to comment.