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 authored and GitHub committed 2024-02-02 14:20:32 +00:00
1 parent abc30517cb
commit a7be5d7b6d
163 files changed
+2867 -5080

No files matched your search

@@ -1,10 +1,6 @@
import { SalesChannelService, SalesChannelModuleService } from "@services"
import { asClass, asValue, createContainer } from "awilix"
import { asValue } from "awilix"
export const mockContainer = createContainer()
mockContainer.register({
transaction: asValue(async (task) => await task()),
export const salesChannelRepositoryMock = {
salesChannelRepository: asValue({
find: jest.fn().mockImplementation(async ({ where: { code } }) => {
return [{}]
@@ -12,6 +8,4 @@ mockContainer.register({
findAndCount: jest.fn().mockResolvedValue([[], 0]),
getFreshManager: jest.fn().mockResolvedValue({}),
}),
salesChannelService: asClass(SalesChannelService),
salesChannelModuleService: asClass(SalesChannelModuleService),
})
}
@@ -1,15 +1,25 @@
import { mockContainer } from "../__fixtures__/sales-channel"
import { createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import ContainerLoader from "../../loaders/container"
import { salesChannelRepositoryMock } from "../__fixtures__/sales-channel"
describe("Sales channel service", function () {
beforeEach(function () {
let container
beforeEach(async function () {
jest.clearAllMocks()
container = createMedusaContainer()
container.register("manager", asValue({}))
await ContainerLoader({ container })
container.register(salesChannelRepositoryMock)
})
it("should list sales channels with filters and relations", async function () {
const salesChannelRepository = mockContainer.resolve(
"salesChannelRepository"
)
const salesChannelService = mockContainer.resolve("salesChannelService")
const salesChannelRepository = container.resolve("salesChannelRepository")
const salesChannelService = container.resolve("salesChannelService")
const config = {
select: ["id", "name"],
@@ -1,2 +1 @@
export { default as SalesChannelService } from "./sales-channel"
export { default as SalesChannelModuleService } from "./sales-channel-module"
@@ -1,48 +1,48 @@
import {
Context,
CreateSalesChannelDTO,
DAL,
FilterableSalesChannelProps,
FindConfig,
InternalModuleDeclaration,
ISalesChannelModuleService,
ModuleJoinerConfig,
RestoreReturn,
ModulesSdkTypes,
SalesChannelDTO,
SoftDeleteReturn,
UpdateSalesChannelDTO,
} from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
mapObjectTo,
MedusaContext,
ModulesSdkUtils,
} from "@medusajs/utils"
import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types"
import { SalesChannel } from "@models"
import SalesChannelService from "./sales-channel"
import {
joinerConfig,
entityNameToLinkableKeysMap,
LinkableKeys,
} from "../joiner-config"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
salesChannelService: SalesChannelService<any>
salesChannelService: ModulesSdkTypes.InternalModuleService<any>
}
export default class SalesChannelModuleService<
TEntity extends SalesChannel = SalesChannel
> implements ISalesChannelModuleService
TEntity extends SalesChannel = SalesChannel
>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
SalesChannelDTO,
{}
>(SalesChannel, [], entityNameToLinkableKeysMap)
implements ISalesChannelModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly salesChannelService_: SalesChannelService<TEntity>
protected readonly salesChannelService_: ModulesSdkTypes.InternalModuleService<TEntity>
constructor(
{ baseRepository, salesChannelService }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.salesChannelService_ = salesChannelService
}
@@ -78,95 +78,6 @@ export default class SalesChannelModuleService<
)
}
async delete(ids: string[], sharedContext?: Context): Promise<void>
async delete(id: string, sharedContext?: Context): Promise<void>
@InjectTransactionManager("baseRepository_")
async delete(
ids: string | string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const salesChannelIds = Array.isArray(ids) ? ids : [ids]
await this.salesChannelService_.delete(salesChannelIds, sharedContext)
}
@InjectTransactionManager("baseRepository_")
protected async softDelete_(
salesChannelIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], Record<string, unknown[]>]> {
return await this.salesChannelService_.softDelete(
salesChannelIds,
sharedContext
)
}
@InjectManager("baseRepository_")
async softDelete<
TReturnableLinkableKeys extends string = Lowercase<
keyof typeof LinkableKeys
>
>(
salesChannelIds: string[],
{ returnLinkableKeys }: SoftDeleteReturn<TReturnableLinkableKeys> = {},
sharedContext: Context = {}
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
const [_, cascadedEntitiesMap] = await this.softDelete_(
salesChannelIds,
sharedContext
)
let mappedCascadedEntitiesMap
if (returnLinkableKeys) {
mappedCascadedEntitiesMap = mapObjectTo<
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
pick: returnLinkableKeys,
})
}
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}
@InjectTransactionManager("baseRepository_")
async restore_(
salesChannelIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], Record<string, unknown[]>]> {
return await this.salesChannelService_.restore(
salesChannelIds,
sharedContext
)
}
@InjectManager("baseRepository_")
async restore<
TReturnableLinkableKeys extends string = Lowercase<
keyof typeof LinkableKeys
>
>(
salesChannelIds: string[],
{ returnLinkableKeys }: RestoreReturn<TReturnableLinkableKeys> = {},
sharedContext: Context = {}
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
const [_, cascadedEntitiesMap] = await this.restore_(
salesChannelIds,
sharedContext
)
let mappedCascadedEntitiesMap
if (returnLinkableKeys) {
mappedCascadedEntitiesMap = mapObjectTo<
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
pick: returnLinkableKeys,
})
}
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}
async update(
data: UpdateSalesChannelDTO[],
sharedContext?: Context
@@ -193,55 +104,4 @@ export default class SalesChannelModuleService<
}
)
}
@InjectManager("baseRepository_")
async retrieve(
salesChannelId: string,
config: FindConfig<SalesChannelDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<SalesChannelDTO> {
const salesChannel = await this.salesChannelService_.retrieve(
salesChannelId,
config
)
return await this.baseRepository_.serialize<SalesChannelDTO>(salesChannel, {
populate: true,
})
}
@InjectManager("baseRepository_")
async list(
filters: {} = {},
config: FindConfig<SalesChannelDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<SalesChannelDTO[]> {
const salesChannels = await this.salesChannelService_.list(filters, config)
return await this.baseRepository_.serialize<SalesChannelDTO[]>(
salesChannels,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCount(
filters: FilterableSalesChannelProps = {},
config: FindConfig<SalesChannelDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[SalesChannelDTO[], number]> {
const [salesChannels, count] = await this.salesChannelService_.listAndCount(
filters,
config
)
return [
await this.baseRepository_.serialize<SalesChannelDTO[]>(salesChannels, {
populate: true,
}),
count,
]
}
}
@@ -1,24 +0,0 @@
import { DAL } from "@medusajs/types"
import { ModulesSdkUtils } from "@medusajs/utils"
import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types"
import { SalesChannel } from "@models"
type InjectedDependencies = {
salesChannelRepository: DAL.RepositoryService
}
export default class SalesChannelService<
TEntity extends SalesChannel = SalesChannel
> extends ModulesSdkUtils.abstractServiceFactory<
InjectedDependencies,
{
create: CreateSalesChannelDTO
update: UpdateSalesChannelDTO
}
>(SalesChannel)<TEntity> {
constructor(container: InjectedDependencies) {
// @ts-ignore
super(...arguments)
}
}
@@ -1,15 +0,0 @@
import { DAL } from "@medusajs/types"
// eslint-disable-next-line @typescript-eslint/no-empty-interface
import { SalesChannel } from "@models"
import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types"
export interface ISalesChannelRepository<
TEntity extends SalesChannel = SalesChannel
> extends DAL.RepositoryService<
TEntity,
{
create: CreateSalesChannelDTO
update: UpdateSalesChannelDTO
}
> {}