feat(prduct, utils, types): Create soft delete pattern for link module (#4649)

* feat(prouct, utils, types): Create soft delete pattern for link module

* add comment

* add comment

* finalise

* remove linkable keys

* cleanup and tests

* cleanup

* add some comments and renaming

* re work

* fix tests

---------

Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
Adrien de Peretti
2023-08-02 19:29:01 +02:00
committed by GitHub
co-authored by Riqwan Thamir
parent fc6c9df035
commit ce3326c5fb
17 changed files with 448 additions and 166 deletions
+1
View File
@@ -1,5 +1,6 @@
export * from "./mikro-orm/mikro-orm-repository"
export * from "./repository"
export * from "./utils"
export * from "./mikro-orm/utils"
export * from "./mikro-orm/mikro-orm-create-connection"
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
@@ -2,10 +2,10 @@ import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
import { MedusaContext } from "../../decorators"
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
import {
mikroOrmSerializer,
mikroOrmUpdateDeletedAtRecursively,
getSoftDeletedCascadedEntitiesIdsMappedBy,
transactionWrapper,
} from "../utils"
import { mikroOrmSerializer, mikroOrmUpdateDeletedAtRecursively } from "./utils"
class MikroOrmBase<T = any> {
protected readonly manager_: any
@@ -71,13 +71,17 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
ids: string[],
@MedusaContext()
{ transactionManager: manager }: Context = {}
): Promise<T[]> {
): Promise<[T[], Record<string, unknown[]>]> {
const entities = await this.find({ where: { id: { $in: ids } } as any })
const date = new Date()
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
return entities
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
entities,
})
return [entities, softDeletedEntitiesMap]
}
@InjectTransactionManager()
+54
View File
@@ -0,0 +1,54 @@
import { SoftDeletableFilterKey } from "./mikro-orm-soft-deletable-filter"
export const mikroOrmUpdateDeletedAtRecursively = async <
T extends object = any
>(
manager: any,
entities: (T & { id: string; deleted_at?: string | Date | null })[],
value: Date | null
) => {
for (const entity of entities) {
if (!("deleted_at" in entity)) continue
entity.deleted_at = value
const relations = manager
.getDriver()
.getMetadata()
.get(entity.constructor.name).relations
const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any)
)
for (const relation of relationsToCascade) {
let collectionRelation = entity[relation.name]
if (!collectionRelation.isInitialized()) {
await collectionRelation.init()
}
const relationEntities = await collectionRelation.getItems({
filters: {
[SoftDeletableFilterKey]: {
withDeleted: true,
},
},
})
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
}
await manager.persist(entity)
}
}
export const mikroOrmSerializer = async <TOutput extends object>(
data: any,
options?: any
): Promise<TOutput> => {
options ??= {}
const { serialize } = await import("@mikro-orm/core")
const result = serialize(data, options)
return result as unknown as Promise<TOutput>
}
+4 -1
View File
@@ -49,7 +49,10 @@ export abstract class AbstractBaseRepository<T = any>
abstract delete(ids: string[], context?: Context): Promise<void>
abstract softDelete(ids: string[], context?: Context): Promise<T[]>
abstract softDelete(
ids: string[],
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
abstract restore(ids: string[], context?: Context): Promise<T[]>
+44 -46
View File
@@ -1,4 +1,4 @@
import { SoftDeletableFilterKey } from "../dal"
import { isObject } from "../common"
export async function transactionWrapper<TManager = unknown>(
this: any,
@@ -33,54 +33,52 @@ export async function transactionWrapper<TManager = unknown>(
return await transactionMethod.bind(this.manager_)(task, options)
}
export const mikroOrmUpdateDeletedAtRecursively = async <
T extends object = any
>(
manager: any,
entities: T[],
value: Date | null
) => {
/**
* Can be used to create a new Object that collect the entities
* based on the columnLookup. This is useful when you want to soft delete entities and return
* an object where the keys are the entities name and the values are the entities
* that were soft deleted.
*
* @param entities
* @param deletedEntitiesMap
* @param getEntityName
*/
export function getSoftDeletedCascadedEntitiesIdsMappedBy({
entities,
deletedEntitiesMap,
getEntityName,
}: {
entities: any[]
deletedEntitiesMap?: Map<string, any[]>
getEntityName?: (entity: any) => string
}): Record<string, any[]> {
deletedEntitiesMap ??= new Map<string, any[]>()
getEntityName ??= (entity) => entity.constructor.name
for (const entity of entities) {
if (!("deleted_at" in entity)) continue
;(entity as any).deleted_at = value
const entityName = getEntityName(entity)
const shouldSkip = !!deletedEntitiesMap
.get(entityName)
?.some((e) => e.id === entity.id)
const relations = manager
.getDriver()
.getMetadata()
.get(entity.constructor.name).relations
const relationsToCascade = relations.filter((relation) =>
relation.cascade.includes("soft-remove" as any)
)
for (const relation of relationsToCascade) {
let collectionRelation = entity[relation.name]
if (!collectionRelation.isInitialized()) {
await collectionRelation.init()
}
const relationEntities = await collectionRelation.getItems({
filters: {
[SoftDeletableFilterKey]: {
withDeleted: true,
},
},
})
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
if (!entity.deleted_at || shouldSkip) {
continue
}
await manager.persist(entity)
}
}
const values = deletedEntitiesMap.get(entityName) ?? []
values.push(entity)
deletedEntitiesMap.set(entityName, values)
export const mikroOrmSerializer = async <TOutput extends object>(
data: any,
options?: any
): Promise<TOutput> => {
options ??= {}
const { serialize } = await import("@mikro-orm/core")
const result = serialize(data, options)
return result as unknown as Promise<TOutput>
Object.values(entity).forEach((propValue: any) => {
if (propValue != null && isObject(propValue[0])) {
getSoftDeletedCascadedEntitiesIdsMappedBy({
entities: propValue,
deletedEntitiesMap,
getEntityName,
})
}
})
}
return Object.fromEntries(deletedEntitiesMap)
}