chore(): Improve internal repository delete algo (#11601)

* chore(): Improve internal repository delete algo

* chore(): Improve internal repository delete algo

* chore(): Improve internal repository delete algo

* update tests

* Create purple-donkeys-learn.md

* update tests
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-02-25 19:59:57 +01:00
1 parent d814d9540e
commit b42f151be3
14 files changed
+89 -82

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/inventory": patch
"@medusajs/link-modules": patch
"@medusajs/product": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
chore(): Improve internal repository delete algo
@@ -70,7 +70,10 @@ export interface RepositoryService<T = any> extends BaseRepositoryService {
context?: Context context?: Context
): Promise<InferRepositoryReturnType<T>[]> ): Promise<InferRepositoryReturnType<T>[]>
delete(idsOrPKs: FindOptions<T>["where"], context?: Context): Promise<void> delete(
idsOrPKs: FindOptions<T>["where"],
context?: Context
): Promise<string[]>
/** /**
* Soft delete entities and cascade to related entities if configured. * Soft delete entities and cascade to related entities if configured.
@@ -127,7 +130,7 @@ export interface TreeRepositoryService<T = any> extends BaseRepositoryService {
context?: Context context?: Context
): Promise<InferRepositoryReturnType<T>[]> ): Promise<InferRepositoryReturnType<T>[]>
delete(ids: string[], context?: Context): Promise<void> delete(ids: string[], context?: Context): Promise<string[]>
} }
/** /**
@@ -64,16 +64,16 @@ export interface IMedusaInternalService<
sharedContext?: Context sharedContext?: Context
): Promise<InferEntityType<TEntity>[]> ): Promise<InferEntityType<TEntity>[]>
delete(idOrSelector: string, sharedContext?: Context): Promise<void> delete(idOrSelector: string, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: string[], sharedContext?: Context): Promise<void> delete(idOrSelector: string[], sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object, sharedContext?: Context): Promise<void> delete(idOrSelector: object, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object[], sharedContext?: Context): Promise<void> delete(idOrSelector: object[], sharedContext?: Context): Promise<string[]>
delete( delete(
idOrSelector: { idOrSelector: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>> selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
}, },
sharedContext?: Context sharedContext?: Context
): Promise<void> ): Promise<string[]>
softDelete( softDelete(
idsOrFilter: idsOrFilter:
@@ -162,7 +162,10 @@ export class MikroOrmBaseRepository<const T extends object = object>
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
delete(idsOrPKs: FindOptions<T>["where"], context?: Context): Promise<void> { delete(
idsOrPKs: FindOptions<T>["where"],
context?: Context
): Promise<string[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -285,7 +288,7 @@ export class MikroOrmBaseTreeRepository<
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
delete(ids: string[], context?: Context): Promise<void> { delete(ids: string[], context?: Context): Promise<string[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
} }
@@ -301,6 +304,10 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> { class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
entity = mikroOrmEntity entity = mikroOrmEntity
tableName = (
(mikroOrmEntity as unknown as EntitySchema).meta ??
(mikroOrmEntity as EntityClass<any>).prototype.__meta
).collection
// @ts-ignore // @ts-ignore
constructor(...args: any[]) { constructor(...args: any[]) {
@@ -428,9 +435,29 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
async delete( async delete(
filters: FindOptions<T>["where"], filters: FindOptions<T>["where"],
context?: Context context?: Context
): Promise<void> { ): Promise<string[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete<T>(this.entity, filters)
const whereSqlInfo = manager
.createQueryBuilder(this.entity.name, this.tableName)
.where(filters)
.getKnexQuery()
.toSQL()
const where = [
whereSqlInfo.sql.split("where ")[1],
whereSqlInfo.bindings,
] as [string, any[]]
return await (manager.getTransactionContext() ?? manager.getKnex())
.queryBuilder()
.from(this.tableName)
.delete()
.where(manager.getKnex().raw(...where))
.returning("id")
.then((rows: { id: string }[]) => {
return rows.map((row: { id: string }) => row.id)
})
} }
async find( async find(
@@ -267,12 +267,7 @@ describe("Internal Module Service Factory", () => {
}) })
it("should delete entities successfully", async () => { it("should delete entities successfully", async () => {
const entitiesToDelete = [{ id: "1", name: "Item" }] await instance.delete({ selector: { id: "1" } })
containerMock[modelRepositoryName].find.mockResolvedValueOnce(
entitiesToDelete
)
await instance.delete({ selector: {} })
expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith(
{ {
$or: [ $or: [
@@ -29,21 +29,21 @@ describe("Abstract Module Service Factory", () => {
mainModelMockService: { mainModelMockService: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
delete: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]), softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]),
}, },
otherModelMock1Service: { otherModelMock1Service: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
delete: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]), softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]),
}, },
otherModelMock2Service: { otherModelMock2Service: {
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
delete: jest.fn().mockResolvedValue(undefined), delete: jest.fn().mockResolvedValue([]),
softDelete: jest.fn().mockResolvedValue([[], {}]), softDelete: jest.fn().mockResolvedValue([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]), restore: jest.fn().mockResolvedValue([[], {}]),
}, },
@@ -367,16 +367,16 @@ export function MedusaInternalService<
) )
} }
delete(idOrSelector: string, sharedContext?: Context): Promise<void> delete(idOrSelector: string, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: string[], sharedContext?: Context): Promise<void> delete(idOrSelector: string[], sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object, sharedContext?: Context): Promise<void> delete(idOrSelector: object, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object[], sharedContext?: Context): Promise<void> delete(idOrSelector: object[], sharedContext?: Context): Promise<string[]>
delete( delete(
idOrSelector: { idOrSelector: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>> selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
}, },
sharedContext?: Context sharedContext?: Context
): Promise<void> ): Promise<string[]>
@InjectTransactionManager(propertyRepositoryName) @InjectTransactionManager(propertyRepositoryName)
async delete( async delete(
@@ -389,12 +389,12 @@ export function MedusaInternalService<
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>> selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
}, },
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<string[]> {
if ( if (
!isDefined(idOrSelector) || !isDefined(idOrSelector) ||
(Array.isArray(idOrSelector) && !idOrSelector.length) (Array.isArray(idOrSelector) && !idOrSelector.length)
) { ) {
return return []
} }
const primaryKeys = AbstractService_.retrievePrimaryKeys(model) const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
@@ -420,21 +420,7 @@ export function MedusaInternalService<
} }
if (isObject(idOrSelector) && "selector" in idOrSelector) { if (isObject(idOrSelector) && "selector" in idOrSelector) {
const entitiesToDelete = await this.list( deleteCriteria.$or.push(idOrSelector.selector)
idOrSelector.selector as FilterQuery<any>,
{
select: primaryKeys,
},
sharedContext
)
for (const entity of entitiesToDelete) {
const criteria = {}
primaryKeys.forEach((key) => {
criteria[key] = entity[key]
})
deleteCriteria.$or.push(criteria)
}
} else { } else {
const primaryKeysValues = Array.isArray(idOrSelector) const primaryKeysValues = Array.isArray(idOrSelector)
? idOrSelector ? idOrSelector
@@ -451,34 +437,18 @@ export function MedusaInternalService<
criteria[primaryKeys[0]] = primaryKeyValue criteria[primaryKeys[0]] = primaryKeyValue
} }
// TODO: Revisit
/*primaryKeys.forEach((key) => {
/!*if (
isObject(primaryKeyValue) &&
!isDefined(primaryKeyValue[key]) &&
// primaryKeys.length > 1
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Composite key must contain all primary key fields: ${primaryKeys.join(
", "
)}. Found: ${Object.keys(primaryKeyValue)}`
)
}*!/
criteria[key] = isObject(primaryKeyValue)
? primaryKeyValue[key]
: primaryKeyValue
})*/
return criteria return criteria
}) })
} }
if (!deleteCriteria.$or.length) { if (!deleteCriteria.$or.length) {
return return []
} }
await this[propertyRepositoryName].delete(deleteCriteria, sharedContext) return await this[propertyRepositoryName].delete(
deleteCriteria,
sharedContext
)
} }
@InjectTransactionManager(propertyRepositoryName) @InjectTransactionManager(propertyRepositoryName)
@@ -277,18 +277,16 @@ export function MedusaService<
? primaryKeyValues ? primaryKeyValues
: [primaryKeyValues] : [primaryKeyValues]
await this.__container__[serviceRegistrationName].delete( const ids = await this.__container__[serviceRegistrationName].delete(
primaryKeyValues_, primaryKeyValues_,
sharedContext sharedContext
) )
primaryKeyValues_.map((primaryKeyValue) => ids.map((id) =>
klassPrototype.aggregatedEvents.bind(this)({ klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.DELETED, action: CommonEvents.DELETED,
object: camelToSnakeCase(modelName).toLowerCase(), object: camelToSnakeCase(modelName).toLowerCase(),
data: isString(primaryKeyValue) data: isString(id) ? { id: id } : id,
? { id: primaryKeyValue }
: primaryKeyValue,
context: sharedContext, context: sharedContext,
}) })
) )
@@ -547,7 +547,7 @@ export default class InventoryModuleService
return return
} }
return await this.inventoryLevelService_.delete(inventoryLevel.id, context) await this.inventoryLevelService_.delete(inventoryLevel.id, context)
} }
// @ts-ignore // @ts-ignore
@@ -17,7 +17,7 @@ export function getLinkRepository(model: EntitySchema) {
this.joinerConfig_ = joinerConfig this.joinerConfig_ = joinerConfig
} }
async delete(data: any, context: Context = {}): Promise<void> { async delete(data: any, context: Context = {}): Promise<string[]> {
const filter = {} const filter = {}
for (const key in data) { for (const key in data) {
filter[key] = { filter[key] = {
@@ -25,8 +25,7 @@ export function getLinkRepository(model: EntitySchema) {
} }
} }
const manager = this.getActiveManager<SqlEntityManager>(context) return await super.delete(filter, context)
await manager.nativeDelete(model, data, {})
} }
async create(data: object[], context: Context = {}): Promise<object[]> { async create(data: object[], context: Context = {}): Promise<object[]> {
@@ -1312,12 +1312,15 @@ moduleIntegrationTestRunner<IProductModuleService>({
relations: ["images"], relations: ["images"],
}) })
const retrievedProductAgain = await service.retrieveProduct(product.id, { const retrievedProductAgain = await service.retrieveProduct(
relations: ["images"], product.id,
}) {
relations: ["images"],
}
)
expect(retrievedProduct.images).toEqual(retrievedProductAgain.images) expect(retrievedProduct.images).toEqual(retrievedProductAgain.images)
expect(retrievedProduct.images).toEqual( expect(retrievedProduct.images).toEqual(
Array.from({ length: 1000 }, (_, i) => Array.from({ length: 1000 }, (_, i) =>
expect.objectContaining({ expect.objectContaining({
@@ -1332,7 +1335,9 @@ moduleIntegrationTestRunner<IProductModuleService>({
// Explicitly verify sequential order // Explicitly verify sequential order
retrievedProduct.images.forEach((img, idx) => { retrievedProduct.images.forEach((img, idx) => {
if (idx > 0) { if (idx > 0) {
expect(img.rank).toBeGreaterThan(retrievedProduct.images[idx - 1].rank) expect(img.rank).toBeGreaterThan(
retrievedProduct.images[idx - 1].rank
)
} }
}) })
}) })
+1 -1
View File
@@ -29,7 +29,7 @@
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
"build": "rimraf dist && tsc --build && npm run resolve:aliases", "build": "rimraf dist && tsc --build && npm run resolve:aliases",
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
"test:integration": "jest --forceExit", "test:integration": "jest --forceExit -- integration-tests/__tests__/**/*.ts",
"migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial", "migration:initial": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
"migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create", "migration:create": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
"migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up", "migration:up": " MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
@@ -272,10 +272,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return [this.sortCategoriesByRank(categoriesTree), count] return [this.sortCategoriesByRank(categoriesTree), count]
} }
async delete(ids: string[], context: Context = {}): Promise<void> { async delete(ids: string[], context: Context = {}): Promise<string[]> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context) await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory.name, { id: ids }, {}) await manager.nativeDelete(ProductCategory.name, { id: ids }, {})
return ids
} }
async softDelete( async softDelete(
@@ -163,8 +163,8 @@ export default class ProductCategoryService {
async delete( async delete(
ids: string[], ids: string[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<string[]> {
await this.productCategoryRepository_.delete(ids, sharedContext) return await this.productCategoryRepository_.delete(ids, sharedContext)
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")