chore: Abstract module service (#6188)

**What**
- Remove services that do not have any custom business and replace them with a simple interfaces
- Abstract module service provide the following base implementation
  - retrieve
  - list
  - listAndCount
  - delete
  - softDelete
  - restore

The above methods are created for the main model and also for each other models for which a config is provided

all method such as list, listAndCount, delete, softDelete and restore are pluralized with the model it refers to

**Migration**
- [x] product
- [x] pricing
- [x] promotion
- [x] cart
- [x] auth
- [x] customer
- [x] payment
- [x] Sales channel
- [x] Workflow-*


**Usage**

**Module**

The module service can now extend the ` ModulesSdkUtils.abstractModuleServiceFactory` which returns a class with the default implementation for each method and each model following the standard naming convention mentioned above.
This factory have 3 template arguments being the container, the main model DTO and an object representing the other model with a config object that contains at list the DTO and optionally a singular and plural property in case it needs to be set manually. It looks like the following:

```ts
export default class PricingModuleService</* ... */>
  extends ModulesSdkUtils.abstractModuleServiceFactory<
    InjectedDependencies,
    PricingTypes.PriceSetDTO,
    {
      Currency: { dto: PricingTypes.CurrencyDTO }
      MoneyAmount: { dto: PricingTypes.MoneyAmountDTO }
      PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO }
      PriceSetMoneyAmountRules: {
        dto: PricingTypes.PriceSetMoneyAmountRulesDTO
      }
      PriceRule: { dto: PricingTypes.PriceRuleDTO }
      RuleType: { dto: PricingTypes.RuleTypeDTO }
      PriceList: { dto: PricingTypes.PriceListDTO }
      PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
    }
  >(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap)
  implements PricingTypes.IPricingModuleService
{
// ...
}
```

In the above, the singular and plural can be inferred as there is no tricky naming. Also, the default implementation does not remove the fact that you need to provides all the overloads etc in your module service interface. The above will provide a default implementation following the interface `AbstractModuleService` which is also auto generated, hence you will have the following methods available:

**for the main model**
- list
- retrieve
- listAndCount 
- delete
- softDelete
- restore


**for the other models**
- list**MyModels**
- retrieve**MyModel**
- listAndCount**MyModels**
- delete**MyModels**
- softDelete**MyModels**
- restore**MyModels**

**Internal module service**

The internal module service can now extend `ModulesSdkUtils.internalModuleServiceFactory` which takes only one template argument which is the container type. 
All internal services provides a default implementation for all retrieve, list, listAndCount, create, update, delete, softDelete, restore methods which follow the following interface `ModulesSdkTypes.InternalModuleService`:

```ts
export interface InternalModuleService<
  TEntity extends {},
  TContainer extends object = object
> {
  get __container__(): TContainer

  retrieve(
    idOrObject: string,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity>
  retrieve(
    idOrObject: object,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity>

  list(
    filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<TEntity[]>

  listAndCount(
    filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
    config?: FindConfig<any>,
    sharedContext?: Context
  ): Promise<[TEntity[], number]>

  create(data: any[], sharedContext?: Context): Promise<TEntity[]>
  create(data: any, sharedContext?: Context): Promise<TEntity>

  update(data: any[], sharedContext?: Context): Promise<TEntity[]>
  update(data: any, sharedContext?: Context): Promise<TEntity>
  update(
    selectorAndData: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
      data: any
    },
    sharedContext?: Context
  ): Promise<TEntity[]>
  update(
    selectorAndData: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
      data: any
    }[],
    sharedContext?: Context
  ): Promise<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: {
      selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
    },
    sharedContext?: Context
  ): Promise<void>

  softDelete(
    idsOrFilter: string[] | InternalFilterQuery,
    sharedContext?: Context
  ): Promise<[TEntity[], Record<string, unknown[]>]>

  restore(
    idsOrFilter: string[] | InternalFilterQuery,
    sharedContext?: Context
  ): Promise<[TEntity[], Record<string, unknown[]>]>

  upsert(data: any[], sharedContext?: Context): Promise<TEntity[]>
  upsert(data: any, sharedContext?: Context): Promise<TEntity>
}
```

When a service is auto generated you can use that interface to type your class property representing the expected internal service.

**Repositories**

The repositories can now extend `DALUtils.mikroOrmBaseRepositoryFactory` which takes one template argument being the entity or the template entity and provides all the default implementation. If the repository is auto generated you can type it using the `RepositoryService` interface. Here is the new interface typings.

```ts
export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
  find(options?: FindOptions<T>, context?: Context): Promise<T[]>

  findAndCount(
    options?: FindOptions<T>,
    context?: Context
  ): Promise<[T[], number]>

  create(data: any[], context?: Context): Promise<T[]>

  // Becareful here, if you have a custom internal service, the update data should never be the entity otherwise
 // both entity and update will point to the same ref and create issues with mikro orm
  update(data: { entity; update }[], context?: Context): Promise<T[]>

  delete(
    idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
    context?: Context
  ): Promise<void>

  /**
   * Soft delete entities and cascade to related entities if configured.
   *
   * @param idsOrFilter
   * @param context
   *
   * @returns [T[], Record<string, string[]>] the second value being the map of the entity names and ids that were soft deleted
   */
  softDelete(
    idsOrFilter: string[] | InternalFilterQuery,
    context?: Context
  ): Promise<[T[], Record<string, unknown[]>]>

  restore(
    idsOrFilter: string[] | InternalFilterQuery,
    context?: Context
  ): Promise<[T[], Record<string, unknown[]>]>

  upsert(data: any[], context?: Context): Promise<T[]>
}
```
This commit is contained in:
Adrien de Peretti
2024-02-02 14:20:32 +00:00
committed by GitHub
parent abc30517cb
commit a7be5d7b6d
163 changed files with 2867 additions and 5080 deletions
-1
View File
@@ -3,5 +3,4 @@ export * from "./mikro-orm/mikro-orm-repository"
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
export * from "./mikro-orm/utils"
export * from "./repositories"
export * from "./repository"
export * from "./utils"
@@ -1,6 +1,8 @@
import {
BaseFilterable,
Context,
DAL,
FilterQuery,
FilterQuery as InternalFilterQuery,
RepositoryService,
RepositoryTransformOptions,
@@ -17,11 +19,11 @@ import {
EntityName,
FilterQuery as MikroFilterQuery,
} from "@mikro-orm/core/typings"
import { MedusaError, isString } from "../../common"
import { isString } from "../../common"
import {
buildQuery,
InjectTransactionManager,
MedusaContext,
buildQuery,
} from "../../modules-sdk"
import {
getSoftDeletedCascadedEntitiesIdsMappedBy,
@@ -42,10 +44,10 @@ export class MikroOrmBase<T = any> {
: this.manager_) as unknown as TManager
}
getActiveManager<TManager = unknown>(
@MedusaContext()
{ transactionManager, manager }: Context = {}
): TManager {
getActiveManager<TManager = unknown>({
transactionManager,
manager,
}: Context = {}): TManager {
return (transactionManager ?? manager ?? this.manager_) as TManager
}
@@ -77,9 +79,10 @@ export class MikroOrmBase<T = any> {
* related ones.
*/
export class MikroOrmBaseRepository<
T extends object = object
> extends MikroOrmBase<T> {
export class MikroOrmBaseRepository<T extends object = object>
extends MikroOrmBase<T>
implements RepositoryService<T>
{
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
@@ -89,11 +92,14 @@ export class MikroOrmBaseRepository<
throw new Error("Method not implemented.")
}
update(data: unknown[], context?: Context): Promise<T[]> {
update(data: { entity; update }[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.")
}
delete(ids: string[] | object[], context?: Context): Promise<void> {
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
context?: Context
): Promise<void> {
throw new Error("Method not implemented.")
}
@@ -115,10 +121,10 @@ export class MikroOrmBaseRepository<
@InjectTransactionManager()
async softDelete(
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext()
{ transactionManager: manager }: Context = {}
@MedusaContext() sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
const isArray = Array.isArray(idsOrFilter)
// TODO handle composite keys
const filter =
isArray || isString(idsOrFilter)
? {
@@ -128,9 +134,10 @@ export class MikroOrmBaseRepository<
}
: idsOrFilter
const entities = await this.find({ where: filter as any })
const entities = await this.find({ where: filter as any }, sharedContext)
const date = new Date()
const manager = this.getActiveManager(sharedContext)
await mikroOrmUpdateDeletedAtRecursively<T>(
manager,
entities as any[],
@@ -147,9 +154,9 @@ export class MikroOrmBaseRepository<
@InjectTransactionManager()
async restore(
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext()
{ transactionManager: manager }: Context = {}
@MedusaContext() sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
// TODO handle composite keys
const isArray = Array.isArray(idsOrFilter)
const filter =
isArray || isString(idsOrFilter)
@@ -164,8 +171,9 @@ export class MikroOrmBaseRepository<
withDeleted: true,
})
const entities = await this.find(query)
const entities = await this.find(query, sharedContext)
const manager = this.getActiveManager(sharedContext)
await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null)
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
@@ -228,18 +236,12 @@ export class MikroOrmBaseTreeRepository<
}
}
type DtoBasedMutationMethods = "create" | "update"
export function mikroOrmBaseRepositoryFactory<
T extends object = object,
TDTOs extends { [K in DtoBasedMutationMethods]?: any } = {
[K in DtoBasedMutationMethods]?: any
}
>(entity: EntityClass<T> | EntitySchema<T>) {
class MikroOrmAbstractBaseRepository_
extends MikroOrmBaseRepository<T>
implements RepositoryService<T, TDTOs>
{
export function mikroOrmBaseRepositoryFactory<T extends object = object>(
entity: any
): {
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
} {
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
// @ts-ignore
constructor(...args: any[]) {
// @ts-ignore
@@ -257,7 +259,7 @@ export function mikroOrmBaseRepositoryFactory<
)
}
async create(data: TDTOs["create"][], context?: Context): Promise<T[]> {
async create(data: any[], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const entities = data.map((data_) => {
@@ -272,76 +274,13 @@ export function mikroOrmBaseRepositoryFactory<
return entities
}
async update(data: TDTOs["update"][], context?: Context): Promise<T[]> {
// TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts
async update(data: { entity; update }[], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const primaryKeys =
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
let primaryKeysCriteria: { [key: string]: any }[] = []
if (primaryKeys.length === 1) {
primaryKeysCriteria.push({
[primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]),
})
} else {
primaryKeysCriteria = data.map((d) => ({
$and: primaryKeys.map((key) => ({ [key]: d[key] })),
}))
}
const allEntities = await Promise.all(
primaryKeysCriteria.map(
async (criteria) =>
await this.find({ where: criteria } as DAL.FindOptions<T>, context)
)
)
const existingEntities = allEntities.flat()
const existingEntitiesMap = new Map<string, T>()
existingEntities.forEach((entity) => {
if (entity) {
const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
entity
)
existingEntitiesMap.set(key, entity)
}
})
const missingEntities = data.filter((data_) => {
const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
data_
)
return !existingEntitiesMap.has(key)
})
if (missingEntities.length) {
const entityName = (entity as EntityClass<T>).name ?? entity
const missingEntitiesKeys = data.map((data_) =>
primaryKeys.map((key) => data_[key]).join(", ")
)
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`${entityName} with ${primaryKeys.join(
", "
)} "${missingEntitiesKeys.join(", ")}" not found`
)
}
const entities = data.map((data_) => {
const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
data_
)
const existingEntity = existingEntitiesMap.get(key)!
return manager.assign(existingEntity, data_ as RequiredEntityData<T>)
return manager.assign(
data_.entity,
data_.update as RequiredEntityData<T>
)
})
manager.persist(entities)
@@ -350,40 +289,11 @@ export function mikroOrmBaseRepositoryFactory<
}
async delete(
primaryKeyValues: string[] | object[],
filters: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
context?: Context
): Promise<void> {
const manager = this.getActiveManager<EntityManager>(context)
const primaryKeys =
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
let deletionCriteria
if (primaryKeys.length > 1) {
deletionCriteria = {
$or: primaryKeyValues.map((compositeKeyValue) => {
const keys = Object.keys(compositeKeyValue)
if (!primaryKeys.every((k) => keys.includes(k))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Composite key must contain all primary key fields: ${primaryKeys.join(
", "
)}. Found: ${keys}`
)
}
const criteria: { [key: string]: any } = {}
for (const key of primaryKeys) {
criteria[key] = compositeKeyValue[key]
}
return criteria
}),
}
} else {
deletionCriteria = { [primaryKeys[0]]: { $in: primaryKeyValues } }
}
await manager.nativeDelete<T>(entity as EntityName<T>, deletionCriteria)
await manager.nativeDelete<T>(entity as EntityName<T>, filters as any)
}
async find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> {
@@ -423,11 +333,7 @@ export function mikroOrmBaseRepositoryFactory<
)
}
async upsert(
data: (TDTOs["create"] | TDTOs["update"])[],
context: Context = {}
): Promise<T[]> {
// TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts
async upsert(data: any[], context: Context = {}): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const primaryKeys =
@@ -435,21 +341,34 @@ export function mikroOrmBaseRepositoryFactory<
let primaryKeysCriteria: { [key: string]: any }[] = []
if (primaryKeys.length === 1) {
primaryKeysCriteria.push({
[primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]),
})
const primaryKeyValues = data
.map((d) => d[primaryKeys[0]])
.filter(Boolean)
if (primaryKeyValues.length) {
primaryKeysCriteria.push({
[primaryKeys[0]]: primaryKeyValues,
})
}
} else {
primaryKeysCriteria = data.map((d) => ({
$and: primaryKeys.map((key) => ({ [key]: d[key] })),
}))
}
const allEntities = await Promise.all(
primaryKeysCriteria.map(
async (criteria) =>
await this.find({ where: criteria } as DAL.FindOptions<T>, context)
let allEntities: T[][] = []
if (primaryKeysCriteria.length) {
allEntities = await Promise.all(
primaryKeysCriteria.map(
async (criteria) =>
await this.find(
{ where: criteria } as DAL.FindOptions<T>,
context
)
)
)
)
}
const existingEntities = allEntities.flat()
@@ -482,7 +401,7 @@ export function mikroOrmBaseRepositoryFactory<
const updatedType = manager.assign(existingEntity, data_)
updatedEntities.push(updatedType)
} else {
const newEntity = manager.create(entity, data_)
const newEntity = manager.create<T>(entity, data_)
createdEntities.push(newEntity)
}
})
@@ -497,9 +416,12 @@ export function mikroOrmBaseRepositoryFactory<
upsertedEntities.push(...updatedEntities)
}
// TODO return the all, created, updated entities
return upsertedEntities
}
}
return MikroOrmAbstractBaseRepository_
return MikroOrmAbstractBaseRepository_ as unknown as {
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
}
}
+31 -2
View File
@@ -1,3 +1,5 @@
import { buildQuery } from "../../modules-sdk"
export const mikroOrmUpdateDeletedAtRecursively = async <
T extends object = any
>(
@@ -27,12 +29,35 @@ export const mikroOrmUpdateDeletedAtRecursively = async <
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 entityRelation.init({ populate: true })
entityRelation = await retrieveEntity()
entityRelation = entityRelation[relation.name]
}
relationEntities = entityRelation.getItems()
} else {
@@ -53,6 +78,10 @@ export const mikroOrmSerializer = async <TOutput extends object>(
): Promise<TOutput> => {
options ??= {}
const { serialize } = await import("@mikro-orm/core")
const result = serialize(data, options)
const result = serialize(data, {
forceObject: true,
populate: true,
...options,
})
return result as unknown as Promise<TOutput>
}
-104
View File
@@ -1,104 +0,0 @@
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
import { MedusaContext } from "../modules-sdk"
import { transactionWrapper } from "./utils"
class AbstractBase<T = any> {
protected readonly manager_: any
protected constructor({ manager }) {
this.manager_ = manager
}
getActiveManager<TManager = unknown>(
@MedusaContext()
{ transactionManager, manager }: Context = {}
): TManager {
return (transactionManager ?? manager ?? this.manager_) as TManager
}
async transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
{
transaction,
isolationLevel,
enableNestedTransactions = false,
}: {
isolationLevel?: string
enableNestedTransactions?: boolean
transaction?: TManager
} = {}
): Promise<any> {
// @ts-ignore
return await transactionWrapper.apply(this, arguments)
}
}
export abstract class AbstractBaseRepository<T = any>
extends AbstractBase
implements DAL.RepositoryService<T>
{
abstract find(options?: DAL.FindOptions<T>, context?: Context)
abstract findAndCount(
options?: DAL.FindOptions<T>,
context?: Context
): Promise<[T[], number]>
abstract create(data: unknown[], context?: Context): Promise<T[]>
abstract update(data: unknown[], context?: Context): Promise<T[]>
abstract delete(ids: string[], context?: Context): Promise<void>
abstract upsert(data: unknown[], context?: Context): Promise<T[]>
abstract softDelete(
ids: string[],
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
abstract restore(
ids: string[],
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
abstract getFreshManager<TManager = unknown>(): TManager
abstract serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput>
}
export abstract class AbstractTreeRepositoryBase<T = any>
extends AbstractBase<T>
implements DAL.TreeRepositoryService<T>
{
protected constructor({ manager }) {
// @ts-ignore
super(...arguments)
}
abstract find(
options?: DAL.FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
context?: Context
)
abstract findAndCount(
options?: DAL.FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<[T[], number]>
abstract create(data: unknown, context?: Context): Promise<T>
abstract delete(id: string, context?: Context): Promise<void>
abstract getFreshManager<TManager = unknown>(): TManager
abstract serialize<TOutput extends object | object[]>(
data: any,
options?: any
): Promise<TOutput>
}