chore: Refactor and improve abstract module service factory (#7688)

* chore: Refactor and improve abstract module service factory

* align naming

* clean up some template args and tests

* partially migrate modules

* partially migrate modules

* migrate more modules

* migrate last modules

* fix typings

* rename interface

* rename interface

* fixes

* fixes

* rm local plain tests
This commit is contained in:
Adrien de Peretti
2024-06-13 13:12:37 +02:00
committed by GitHub
parent c57223a3a2
commit d2a5201eeb
44 changed files with 590 additions and 519 deletions

View File

@@ -1,4 +1,4 @@
import { internalModuleServiceFactory } from "../internal-module-service-factory"
import { MedusaInternalService } from "../medusa-internal-service"
import { lowerCaseFirst } from "../../common"
const defaultContext = { __type: "MedusaContext" }
@@ -39,14 +39,14 @@ describe("Internal Module Service Factory", () => {
},
}
const internalModuleService = internalModuleServiceFactory<any>(Model)
const IMedusaInternalService = MedusaInternalService<any>(Model)
describe("Internal Module Service Methods", () => {
let instance
beforeEach(() => {
jest.clearAllMocks()
instance = new internalModuleService(containerMock)
instance = new IMedusaInternalService(containerMock)
})
it("should throw model id undefined error on retrieve if id is not defined", async () => {
@@ -62,10 +62,10 @@ describe("Internal Module Service Factory", () => {
static meta = { primaryKeys: ["id", "name"] }
}
const compositeInternalModuleService =
internalModuleServiceFactory<any>(CompositeModel)
const compositeIMedusaInternalService =
MedusaInternalService<any>(CompositeModel)
const instance = new compositeInternalModuleService(containerMock)
const instance = new compositeIMedusaInternalService(containerMock)
const err = await instance.retrieve().catch((e) => e)
expect(err.message).toBe("compositeModel - id, name must be defined")
@@ -94,10 +94,10 @@ describe("Internal Module Service Factory", () => {
static meta = { primaryKeys: ["id", "name"] }
}
const compositeInternalModuleService =
internalModuleServiceFactory<any>(CompositeModel)
const compositeIMedusaInternalService =
MedusaInternalService<any>(CompositeModel)
const instance = new compositeInternalModuleService(containerMock)
const instance = new compositeIMedusaInternalService(containerMock)
const entity = { id: "1", name: "Item" }
containerMock[

View File

@@ -1,4 +1,4 @@
import { abstractModuleServiceFactory } from "../abstract-module-service-factory"
import { MedusaService } from "../medusa-service"
const baseRepoMock = {
serialize: jest.fn().mockImplementation((item) => item),
@@ -41,13 +41,12 @@ describe("Abstract Module Service Factory", () => {
},
}
const mainModelMock = class MainModelMock {}
const otherModelMock1 = class OtherModelMock1 {}
const otherModelMock2 = class OtherModelMock2 {}
class MainModelMock {}
class OtherModelMock1 {}
class OtherModelMock2 {}
const abstractModuleService = abstractModuleServiceFactory<
any,
any,
const abstractModuleService = MedusaService<
MainModelMock,
{
OtherModelMock1: {
dto: any
@@ -60,22 +59,10 @@ describe("Abstract Module Service Factory", () => {
plural: "OtherModelMock2s"
}
}
>(
mainModelMock,
[
{
model: otherModelMock1,
plural: "otherModelMock1s",
singular: "otherModelMock1",
},
{
model: otherModelMock2,
plural: "otherModelMock2s",
singular: "otherModelMock2",
},
]
// Add more parameters as needed
)
>(MainModelMock, {
OtherModelMock1,
OtherModelMock2,
})
describe("Main Model Methods", () => {
let instance

View File

@@ -6,7 +6,7 @@ export * from "./loaders/mikro-orm-connection-loader-factory"
export * from "./loaders/container-loader-factory"
export * from "./create-pg-connection"
export * from "./migration-scripts"
export * from "./internal-module-service-factory"
export * from "./abstract-module-service-factory"
export * from "./medusa-internal-service"
export * from "./medusa-service"
export * from "./definition"
export * from "./event-builder-factory"

View File

@@ -8,7 +8,7 @@ import {
} from "@medusajs/types"
import { asClass } from "awilix"
import { internalModuleServiceFactory } from "../internal-module-service-factory"
import { MedusaInternalService } from "../medusa-internal-service"
import { lowerCaseFirst } from "../../common"
import {
MikroOrmBaseRepository,
@@ -100,10 +100,7 @@ export function loadModuleServices({
const finalService = moduleServicesMap.get(mappedServiceName)
if (!finalService) {
moduleServicesMap.set(
mappedServiceName,
internalModuleServiceFactory(Model)
)
moduleServicesMap.set(mappedServiceName, MedusaInternalService(Model))
}
})

View File

@@ -33,20 +33,18 @@ type SelectorAndData = {
data: any
}
export function internalModuleServiceFactory<
TContainer extends object = object
>(
export function MedusaInternalService<TContainer extends object = object>(
model: any
): {
new <TEntity extends object = any>(
container: TContainer
): ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
): ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
} {
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
const propertyRepositoryName = `__${injectedRepositoryName}__`
class AbstractService_<TEntity extends object>
implements ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
implements ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
{
readonly __container__: TContainer;
[key: string]: any
@@ -525,5 +523,5 @@ export function internalModuleServiceFactory<
return AbstractService_ as unknown as new <TEntity extends {}>(
container: TContainer
) => ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
) => ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
}

View File

@@ -12,11 +12,11 @@ import {
SoftDeleteReturn,
} from "@medusajs/types"
import {
MapToConfig,
isString,
kebabCase,
lowerCaseFirst,
mapObjectTo,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
@@ -25,6 +25,7 @@ import {
InjectTransactionManager,
MedusaContext,
} from "./decorators"
import { ModuleRegistrationName } from "./definition"
type BaseMethods =
| "retrieve"
@@ -49,68 +50,74 @@ const methods: BaseMethods[] = [...readMethods, ...writeMethods]
type ModelDTOConfig = {
dto: object
create?: object
update?: object
}
type ModelDTOConfigRecord = Record<any, ModelDTOConfig>
type ModelNamingConfig = {
create?: any
update?: any
/**
* @internal
*/
singular?: string
/**
* @internal
*/
plural?: string
}
type ModelsConfigTemplate = {
[ModelName: string]: ModelDTOConfig & ModelNamingConfig
type EntitiesConfigTemplate = { [key: string]: ModelDTOConfig }
type ModelConfigurationToDto<T extends ModelConfiguration> =
T extends abstract new (...args: any) => infer R
? R
: T extends { dto: infer DTO }
? DTO
: any
type ModelConfigurationsToConfigTemplate<
T extends Record<string, ModelConfiguration>
> = {
[Key in keyof T as `${Capitalize<Key & string>}`]: {
dto: ModelConfigurationToDto<T[Key]>
create: any
update: any
}
}
type ExtractSingularName<
T extends Record<any, any>,
K = keyof T
> = T[K] extends { singular?: string } ? T[K]["singular"] : K
type CreateMethodName<
TModelDTOConfig extends ModelDTOConfigRecord,
TModelName = keyof TModelDTOConfig
> = TModelDTOConfig[TModelName] extends { create?: object }
? `create${ExtractPluralName<TModelDTOConfig, TModelName>}`
: never
type UpdateMethodName<
TModelDTOConfig extends ModelDTOConfigRecord,
TModelName = keyof TModelDTOConfig
> = TModelDTOConfig[TModelName] extends { update?: object }
? `update${ExtractPluralName<TModelDTOConfig, TModelName>}`
: never
type ExtractSingularName<T extends Record<any, any>, K = keyof T> = Capitalize<
T[K] extends { singular?: string } ? T[K]["singular"] & string : K & string
>
type ExtractPluralName<T extends Record<any, any>, K = keyof T> = T[K] extends {
plural?: string
}
? T[K]["plural"]
? T[K]["plural"] & string
: Pluralize<K & string>
type ModelConfiguration =
| Constructor<any>
| { singular?: string; plural?: string; model: Constructor<any> }
// TODO: this will be removed in the follow up pr once the main entity concept will be removed
type ModelConfiguration = Constructor<any> | ModelDTOConfig | any
export interface AbstractModuleServiceBase<TContainer, TMainModelDTO> {
get __container__(): TContainer
type ExtractMutationDtoOrAny<T> = T extends unknown ? any : T
export interface AbstractModuleServiceBase<TEntryEntityConfig> {
new (container: Record<any, any>, ...args: any[]): this
get __container__(): Record<any, any>
retrieve(
id: string,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TMainModelDTO>
): Promise<TEntryEntityConfig>
list(
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TMainModelDTO[]>
): Promise<TEntryEntityConfig[]>
listAndCount(
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<[TMainModelDTO[], number]>
): Promise<[TEntryEntityConfig[], number]>
delete(
primaryKeyValues: string | object | string[] | object[],
@@ -131,56 +138,50 @@ export interface AbstractModuleServiceBase<TContainer, TMainModelDTO> {
}
export type AbstractModuleService<
TContainer,
TMainModelDTO,
TModelDTOConfig extends ModelsConfigTemplate
> = AbstractModuleServiceBase<TContainer, TMainModelDTO> & {
[TModelName in keyof TModelDTOConfig as `retrieve${ExtractSingularName<
TModelDTOConfig,
TModelName
> &
string}`]: (
TEntryEntityConfig extends ModelConfiguration,
TEntitiesDtoConfig extends EntitiesConfigTemplate
> = AbstractModuleServiceBase<TEntryEntityConfig> & {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
id: string,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TModelDTOConfig[TModelName & string]["dto"]>
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
} & {
[TModelName in keyof TModelDTOConfig as `list${ExtractPluralName<
TModelDTOConfig,
TModelName
> &
string}`]: (
[TEntityName in keyof TEntitiesDtoConfig as `list${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TModelDTOConfig[TModelName & string]["dto"][]>
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
} & {
[TModelName in keyof TModelDTOConfig as `listAndCount${ExtractPluralName<
TModelDTOConfig,
TModelName
> &
string}`]: {
[TEntityName in keyof TEntitiesDtoConfig as `listAndCount${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
[TModelDTOConfig[TModelName & string]["dto"][], number]
[TEntitiesDtoConfig[TEntityName]["dto"][], number]
>
}
} & {
[TModelName in keyof TModelDTOConfig as `delete${ExtractPluralName<
TModelDTOConfig,
TModelName
> &
string}`]: {
[TEntityName in keyof TEntitiesDtoConfig as `delete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
}
} & {
[TModelName in keyof TModelDTOConfig as `softDelete${ExtractPluralName<
TModelDTOConfig,
TModelName
> &
string}`]: {
[TEntityName in keyof TEntitiesDtoConfig as `softDelete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
@@ -188,11 +189,10 @@ export type AbstractModuleService<
): Promise<Record<string, string[]> | void>
}
} & {
[TModelName in keyof TModelDTOConfig as `restore${ExtractPluralName<
TModelDTOConfig,
TModelName
> &
string}`]: {
[TEntityName in keyof TEntitiesDtoConfig as `restore${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
@@ -200,45 +200,103 @@ export type AbstractModuleService<
): Promise<Record<string, string[]> | void>
}
} & {
[TModelName in keyof TModelDTOConfig as CreateMethodName<
TModelDTOConfig,
TModelName
>]: {
(
data: TModelDTOConfig[TModelName]["create"][],
sharedContext?: Context
): Promise<TModelDTOConfig[TModelName]["dto"][]>
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
} & {
[TModelName in keyof TModelDTOConfig as CreateMethodName<
TModelDTOConfig,
TModelName
>]: {
(
data: TModelDTOConfig[TModelName]["create"],
sharedContext?: Context
): Promise<TModelDTOConfig[TModelName]["dto"]>
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
}
// TODO: Because of a bug, those methods were not made visible which now cause issues with the fix as our interface are not consistent with the expectations
// are not consistent accross modules
/* & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any[], sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TModelName in keyof TModelDTOConfig as UpdateMethodName<
TModelDTOConfig,
TModelName
>]: {
(
data: TModelDTOConfig[TModelName]["update"][],
sharedContext?: Context
): Promise<TModelDTOConfig[TModelName]["dto"][]>
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any, sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TModelName in keyof TModelDTOConfig as UpdateMethodName<
TModelDTOConfig,
TModelName
>]: {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TModelDTOConfig[TModelName]["update"],
data: TEntitiesDtoConfig[TEntityName]["update"][],
sharedContext?: Context
): Promise<TModelDTOConfig[TModelName]["dto"]>
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
idOrdSelector: any,
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
}*/
/**
* @internal
*/
function buildMethodNamesFromModel(
model: ModelConfiguration,
suffixed: boolean = true
): Record<string, string> {
return methods.reduce((acc, method) => {
let modelName: string = ""
if (method === "retrieve") {
modelName =
"singular" in model && model.singular
? model.singular
: (model as { name: string }).name
} else {
modelName =
"plural" in model && model.plural
? model.plural
: pluralize((model as { name: string }).name)
}
const methodName = suffixed
? `${method}${upperCaseFirst(modelName)}`
: method
return { ...acc, [method]: methodName }
}, {})
}
/**
@@ -246,7 +304,7 @@ export type AbstractModuleService<
*
* @example
*
* const otherModels = new Set([
* const entities = {
* Currency,
* Price,
* PriceList,
@@ -255,12 +313,10 @@ export type AbstractModuleService<
* PriceRule,
* PriceSetRuleType,
* RuleType,
* ])
* }
*
* const AbstractModuleService = ModulesSdkUtils.abstractModuleServiceFactory<
* InjectedDependencies,
* class MyService extends ModulesSdkUtils.MedusaService<
* PricingTypes.PriceSetDTO,
* // The configuration of each entity also accept singular/plural properties, if not provided then it is using english pluralization
* {
* Currency: { dto: PricingTypes.CurrencyDTO }
* Price: { dto: PricingTypes.PriceDTO }
@@ -269,61 +325,55 @@ export type AbstractModuleService<
* PriceList: { dto: PricingTypes.PriceListDTO }
* PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
* }
* >(PriceSet, [...otherModels], entityNameToLinkableKeysMap)
* >(PriceSet, entities, entityNameToLinkableKeysMap) {}
*
* @param mainModel
* @param otherModels
* @example
*
* // Here the DTO's and names will be inferred from the arguments
*
* const entities = {
* Currency,
* Price,
* PriceList,
* PriceListRule,
* PriceListRuleValue,
* PriceRule,
* PriceSetRuleType,
* RuleType,
* }
*
* class MyService extends ModulesSdkUtils.MedusaService(PriceSet, entities, entityNameToLinkableKeysMap) {}
*
* @param entryEntity
* @param entities
* @param entityNameToLinkableKeysMap
*/
export function abstractModuleServiceFactory<
TContainer,
TMainModelDTO,
ModelsConfig extends ModelsConfigTemplate
export function MedusaService<
TEntryEntityConfig extends ModelConfiguration = ModelConfiguration,
EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
TEntities extends Record<string, ModelConfiguration> = Record<
string,
ModelConfiguration
>
>(
mainModel: Constructor<any>,
otherModels: ModelConfiguration[],
entryEntity: (TEntryEntityConfig & { name: string }) | Constructor<any>,
entities: TEntities,
entityNameToLinkableKeysMap: MapToConfig = {}
): {
new (container: TContainer): AbstractModuleService<
TContainer,
TMainModelDTO,
ModelsConfig
new (...args: any[]): AbstractModuleService<
ModelConfigurationToDto<TEntryEntityConfig>,
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>
} {
const buildMethodNamesFromModel = (
model: ModelConfiguration,
suffixed: boolean = true
): Record<string, string> => {
return methods.reduce((acc, method) => {
let modelName: string = ""
if (method === "retrieve") {
modelName =
"singular" in model && model.singular
? model.singular
: (model as Constructor<any>).name
} else {
modelName =
"plural" in model && model.plural
? model.plural
: pluralize((model as Constructor<any>).name)
}
const methodName = suffixed
? `${method}${upperCaseFirst(modelName)}`
: method
return { ...acc, [method]: methodName }
}, {})
}
const buildAndAssignMethodImpl = function (
klassPrototype: any,
method: string,
methodName: string,
model: Constructor<any>
modelName: string
): void {
const serviceRegistrationName = `${lowerCaseFirst(model.name)}Service`
const serviceRegistrationName = `${lowerCaseFirst(modelName)}Service`
const applyMethod = function (impl: Function, contextIndex) {
klassPrototype[methodName] = impl
@@ -465,7 +515,7 @@ export function abstractModuleServiceFactory<
await this.eventBusModuleService_?.emit(
primaryKeyValues_.map((primaryKeyValue) => ({
eventName: `${kebabCase(model.name)}.deleted`,
eventName: `${kebabCase(modelName)}.deleted`,
data: isString(primaryKeyValue)
? { id: primaryKeyValue }
: primaryKeyValue,
@@ -500,7 +550,7 @@ export function abstractModuleServiceFactory<
await this.eventBusModuleService_?.emit(
softDeletedEntities.map(({ id }) => ({
eventName: `${kebabCase(model.name)}.deleted`,
eventName: `${kebabCase(modelName)}.deleted`,
metadata: { source: "", action: "", object: "" },
data: { id },
}))
@@ -558,19 +608,19 @@ export function abstractModuleServiceFactory<
}
class AbstractModuleService_ {
readonly __container__: Record<string, any>
readonly __container__: Record<any, any>
readonly baseRepository_: RepositoryService
readonly eventBusModuleService_: IEventBusModuleService;
[key: string]: any
constructor(container: Record<string, any>) {
constructor(container: Record<any, any>) {
this.__container__ = container
this.baseRepository_ = container.baseRepository
const hasEventBusModuleService = Object.keys(this.__container__).find(
// TODO: Should use ModuleRegistrationName.EVENT_BUS but it would require to move it to the utils package to prevent circular dependencies
(key) => key === "eventBusModuleService"
(key) => key === ModuleRegistrationName.EVENT_BUS
)
this.eventBusModuleService_ = hasEventBusModuleService
@@ -592,18 +642,18 @@ export function abstractModuleServiceFactory<
}
}
const mainModelMethods = buildMethodNamesFromModel(mainModel, false)
const entryEntityMethods = buildMethodNamesFromModel(entryEntity, false)
/**
* Build the main retrieve/list/listAndCount/delete/softDelete/restore methods for the main model
*/
for (let [method, methodName] of Object.entries(mainModelMethods)) {
for (let [method, methodName] of Object.entries(entryEntityMethods)) {
buildAndAssignMethodImpl(
AbstractModuleService_.prototype,
method,
methodName,
mainModel
entryEntity.name
)
}
@@ -611,22 +661,26 @@ export function abstractModuleServiceFactory<
* Build the retrieve/list/listAndCount/delete/softDelete/restore methods for all the other models
*/
const otherModelsMethods: [ModelConfiguration, Record<string, string>][] =
otherModels.map((model) => [model, buildMethodNamesFromModel(model)])
const entitiesMethods: [
string,
ModelConfiguration,
Record<string, string>
][] = Object.entries(entities).map(([name, config]) => [
name,
config,
buildMethodNamesFromModel(config),
])
for (let [model, modelsMethods] of otherModelsMethods) {
for (let [modelName, model, modelsMethods] of entitiesMethods) {
Object.entries(modelsMethods).forEach(([method, methodName]) => {
model = "model" in model ? model.model : model
buildAndAssignMethodImpl(
AbstractModuleService_.prototype,
method,
methodName,
model
modelName
)
})
}
return AbstractModuleService_ as unknown as new (
container: TContainer
) => AbstractModuleService<TContainer, TMainModelDTO, ModelsConfig>
return AbstractModuleService_ as any
}