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
2025-02-25 19:59:57 +01:00
committed by GitHub
parent d814d9540e
commit b42f151be3
14 changed files with 89 additions and 82 deletions

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

View File

@@ -70,7 +70,10 @@ export interface RepositoryService<T = any> extends BaseRepositoryService {
context?: Context
): 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.
@@ -127,7 +130,7 @@ export interface TreeRepositoryService<T = any> extends BaseRepositoryService {
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
delete(ids: string[], context?: Context): Promise<void>
delete(ids: string[], context?: Context): Promise<string[]>
}
/**

View File

@@ -64,16 +64,16 @@ export interface IMedusaInternalService<
sharedContext?: Context
): Promise<InferEntityType<TEntity>[]>
delete(idOrSelector: string, sharedContext?: Context): Promise<void>
delete(idOrSelector: string[], sharedContext?: Context): Promise<void>
delete(idOrSelector: object, sharedContext?: Context): Promise<void>
delete(idOrSelector: object[], sharedContext?: Context): Promise<void>
delete(idOrSelector: string, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: string[], sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object[], sharedContext?: Context): Promise<string[]>
delete(
idOrSelector: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
},
sharedContext?: Context
): Promise<void>
): Promise<string[]>
softDelete(
idsOrFilter:

View File

@@ -162,7 +162,10 @@ export class MikroOrmBaseRepository<const T extends object = object>
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.")
}
@@ -285,7 +288,7 @@ export class MikroOrmBaseTreeRepository<
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.")
}
}
@@ -301,6 +304,10 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
entity = mikroOrmEntity
tableName = (
(mikroOrmEntity as unknown as EntitySchema).meta ??
(mikroOrmEntity as EntityClass<any>).prototype.__meta
).collection
// @ts-ignore
constructor(...args: any[]) {
@@ -428,9 +435,29 @@ export function mikroOrmBaseRepositoryFactory<const T extends object>(
async delete(
filters: FindOptions<T>["where"],
context?: Context
): Promise<void> {
const manager = this.getActiveManager<EntityManager>(context)
await manager.nativeDelete<T>(this.entity, filters)
): Promise<string[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
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(

View File

@@ -267,12 +267,7 @@ describe("Internal Module Service Factory", () => {
})
it("should delete entities successfully", async () => {
const entitiesToDelete = [{ id: "1", name: "Item" }]
containerMock[modelRepositoryName].find.mockResolvedValueOnce(
entitiesToDelete
)
await instance.delete({ selector: {} })
await instance.delete({ selector: { id: "1" } })
expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith(
{
$or: [

View File

@@ -29,21 +29,21 @@ describe("Abstract Module Service Factory", () => {
mainModelMockService: {
retrieve: 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([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
otherModelMock1Service: {
retrieve: 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([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},
otherModelMock2Service: {
retrieve: 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([[], {}]),
restore: jest.fn().mockResolvedValue([[], {}]),
},

View File

@@ -367,16 +367,16 @@ export function MedusaInternalService<
)
}
delete(idOrSelector: string, sharedContext?: Context): Promise<void>
delete(idOrSelector: string[], sharedContext?: Context): Promise<void>
delete(idOrSelector: object, sharedContext?: Context): Promise<void>
delete(idOrSelector: object[], sharedContext?: Context): Promise<void>
delete(idOrSelector: string, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: string[], sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object, sharedContext?: Context): Promise<string[]>
delete(idOrSelector: object[], sharedContext?: Context): Promise<string[]>
delete(
idOrSelector: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
},
sharedContext?: Context
): Promise<void>
): Promise<string[]>
@InjectTransactionManager(propertyRepositoryName)
async delete(
@@ -389,12 +389,12 @@ export function MedusaInternalService<
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
},
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
): Promise<string[]> {
if (
!isDefined(idOrSelector) ||
(Array.isArray(idOrSelector) && !idOrSelector.length)
) {
return
return []
}
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
@@ -420,21 +420,7 @@ export function MedusaInternalService<
}
if (isObject(idOrSelector) && "selector" in idOrSelector) {
const entitiesToDelete = await this.list(
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)
}
deleteCriteria.$or.push(idOrSelector.selector)
} else {
const primaryKeysValues = Array.isArray(idOrSelector)
? idOrSelector
@@ -451,34 +437,18 @@ export function MedusaInternalService<
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
})
}
if (!deleteCriteria.$or.length) {
return
return []
}
await this[propertyRepositoryName].delete(deleteCriteria, sharedContext)
return await this[propertyRepositoryName].delete(
deleteCriteria,
sharedContext
)
}
@InjectTransactionManager(propertyRepositoryName)

View File

@@ -277,18 +277,16 @@ export function MedusaService<
? primaryKeyValues
: [primaryKeyValues]
await this.__container__[serviceRegistrationName].delete(
const ids = await this.__container__[serviceRegistrationName].delete(
primaryKeyValues_,
sharedContext
)
primaryKeyValues_.map((primaryKeyValue) =>
ids.map((id) =>
klassPrototype.aggregatedEvents.bind(this)({
action: CommonEvents.DELETED,
object: camelToSnakeCase(modelName).toLowerCase(),
data: isString(primaryKeyValue)
? { id: primaryKeyValue }
: primaryKeyValue,
data: isString(id) ? { id: id } : id,
context: sharedContext,
})
)

View File

@@ -547,7 +547,7 @@ export default class InventoryModuleService
return
}
return await this.inventoryLevelService_.delete(inventoryLevel.id, context)
await this.inventoryLevelService_.delete(inventoryLevel.id, context)
}
// @ts-ignore

View File

@@ -17,7 +17,7 @@ export function getLinkRepository(model: EntitySchema) {
this.joinerConfig_ = joinerConfig
}
async delete(data: any, context: Context = {}): Promise<void> {
async delete(data: any, context: Context = {}): Promise<string[]> {
const filter = {}
for (const key in data) {
filter[key] = {
@@ -25,8 +25,7 @@ export function getLinkRepository(model: EntitySchema) {
}
}
const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete(model, data, {})
return await super.delete(filter, context)
}
async create(data: object[], context: Context = {}): Promise<object[]> {

View File

@@ -1312,12 +1312,15 @@ moduleIntegrationTestRunner<IProductModuleService>({
relations: ["images"],
})
const retrievedProductAgain = await service.retrieveProduct(product.id, {
relations: ["images"],
})
const retrievedProductAgain = await service.retrieveProduct(
product.id,
{
relations: ["images"],
}
)
expect(retrievedProduct.images).toEqual(retrievedProductAgain.images)
expect(retrievedProduct.images).toEqual(
Array.from({ length: 1000 }, (_, i) =>
expect.objectContaining({
@@ -1332,7 +1335,9 @@ moduleIntegrationTestRunner<IProductModuleService>({
// Explicitly verify sequential order
retrievedProduct.images.forEach((img, idx) => {
if (idx > 0) {
expect(img.rank).toBeGreaterThan(retrievedProduct.images[idx - 1].rank)
expect(img.rank).toBeGreaterThan(
retrievedProduct.images[idx - 1].rank
)
}
})
})

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",
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
"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: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",

View File

@@ -272,10 +272,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
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)
await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory.name, { id: ids }, {})
return ids
}
async softDelete(

View File

@@ -163,8 +163,8 @@ export default class ProductCategoryService {
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.productCategoryRepository_.delete(ids, sharedContext)
): Promise<string[]> {
return await this.productCategoryRepository_.delete(ids, sharedContext)
}
@InjectTransactionManager("productCategoryRepository_")