chore: Abstract module services (#6087)

**What**
Create a service abstraction for the modules internal service layer. The objective is to reduce the effort of building new modules when the logic is the same or otherwise allow to override the default behavior.

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-01-18 10:20:08 +01:00
committed by GitHub
parent 80feb972cb
commit 130c641e5c
46 changed files with 857 additions and 2836 deletions

View File

@@ -1,7 +1,7 @@
import {
Context,
DAL,
FilterQuery as InternalFilerQuery,
FilterQuery as InternalFilterQuery,
RepositoryTransformOptions,
} from "@medusajs/types"
import {
@@ -16,9 +16,9 @@ import {
EntityName,
FilterQuery as MikroFilterQuery,
} from "@mikro-orm/core/typings"
import { isString, MedusaError } from "../../common"
import { MedusaError, isString } from "../../common"
import { MedusaContext } from "../../decorators"
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
import { InjectTransactionManager, buildQuery } from "../../modules-sdk"
import {
getSoftDeletedCascadedEntitiesIdsMappedBy,
transactionWrapper,
@@ -106,7 +106,7 @@ export class MikroOrmBaseRepository<
@InjectTransactionManager()
async softDelete(
idsOrFilter: string[] | InternalFilerQuery,
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext()
{ transactionManager: manager }: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
@@ -138,7 +138,7 @@ export class MikroOrmBaseRepository<
@InjectTransactionManager()
async restore(
idsOrFilter: string[] | InternalFilerQuery,
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext()
{ transactionManager: manager }: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
@@ -224,7 +224,7 @@ type DtoBasedMutationMethods = "create" | "update"
export function mikroOrmBaseRepositoryFactory<
T extends object = object,
TDTos extends { [K in DtoBasedMutationMethods]?: any } = {
TDTOs extends { [K in DtoBasedMutationMethods]?: any } = {
[K in DtoBasedMutationMethods]?: any
}
>(entity: EntityClass<T> | EntitySchema<T>) {
@@ -242,11 +242,11 @@ export function mikroOrmBaseRepositoryFactory<
static retrievePrimaryKeys(entity: EntityClass<T> | EntitySchema<T>) {
return (
(entity as EntitySchema<T>).meta?.primaryKeys ??
(entity as EntityClass<T>).prototype.__meta.primaryKeys
(entity as EntityClass<T>).prototype.__meta.primaryKeys ?? ["id"]
)
}
async create(data: TDTos["create"][], context?: Context): Promise<T[]> {
async create(data: TDTOs["create"][], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const entities = data.map((data_) => {
@@ -261,7 +261,8 @@ export function mikroOrmBaseRepositoryFactory<
return entities
}
async update(data: TDTos["update"][], context?: Context): Promise<T[]> {
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
const manager = this.getActiveManager<EntityManager>(context)
const primaryKeys =

View File

@@ -0,0 +1,245 @@
import {
Context,
FilterQuery as InternalFilterQuery,
FindConfig,
} from "@medusajs/types"
import { EntitySchema } from "@mikro-orm/core"
import { EntityClass } from "@mikro-orm/core/typings"
import {
doNotForceTransaction,
isDefined,
isString,
lowerCaseFirst,
MedusaError,
shouldForceTransaction,
upperCaseFirst,
} from "../common"
import { MedusaContext } from "../decorators"
import { buildQuery } from "./build-query"
import { InjectManager, InjectTransactionManager } from "./decorators"
/**
* Utility factory and interfaces for internal module services
*/
type FilterableMethods = "list" | "listAndCount"
type Methods = "create" | "update"
export interface AbstractService<
TEntity extends {},
TContainer extends object = object,
TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any },
TFilters extends { [K in FilterableMethods]?: any } = {
[K in FilterableMethods]?: any
}
> {
get __container__(): TContainer
retrieve<TEntityMethod = TEntity>(
id: string,
config?: FindConfig<TEntityMethod>,
sharedContext?: Context
): Promise<TEntity>
list<TEntityMethod = TEntity>(
filters?: TFilters["list"],
config?: FindConfig<TEntityMethod>,
sharedContext?: Context
): Promise<TEntity[]>
listAndCount<TEntityMethod = TEntity>(
filters?: TFilters["listAndCount"],
config?: FindConfig<TEntityMethod>,
sharedContext?: Context
): Promise<[TEntity[], number]>
create(data: TDTOs["create"][], sharedContext?: Context): Promise<TEntity[]>
update(data: TDTOs["update"][], sharedContext?: Context): Promise<TEntity[]>
delete(
primaryKeyValues: string[] | object[],
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[]>]>
}
export function abstractServiceFactory<
TContainer extends object = object,
TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any },
TFilters extends { [K in FilterableMethods]?: any } = {
[K in FilterableMethods]?: any
}
>(
model: new (...args: any[]) => any
): {
new <TEntity extends {}>(container: TContainer): AbstractService<
TEntity,
TContainer,
TDTOs,
TFilters
>
} {
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
const propertyRepositoryName = `__${injectedRepositoryName}__`
class AbstractService_<TEntity extends {}>
implements AbstractService<TEntity, TContainer, TDTOs, TFilters>
{
readonly __container__: TContainer;
[key: string]: any
constructor(container: TContainer) {
this.__container__ = container
this[propertyRepositoryName] = container[injectedRepositoryName]
}
static retrievePrimaryKeys(entity: EntityClass<any> | EntitySchema<any>) {
return (
(entity as EntitySchema<any>).meta?.primaryKeys ??
(entity as EntityClass<any>).prototype.__meta?.primaryKeys ?? ["id"]
)
}
@InjectManager(propertyRepositoryName)
async retrieve<TEntityMethod = TEntity>(
primaryKeyValues: string | string[] | object[],
config: FindConfig<TEntityMethod> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
if (!isDefined(primaryKeyValues)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`${
primaryKeys.length === 1
? `"${
lowerCaseFirst(model.name) + upperCaseFirst(primaryKeys[0])
}"`
: `${lowerCaseFirst(model.name)} ${primaryKeys.join(", ")}`
} must be defined`
)
}
let primaryKeysCriteria = {}
if (primaryKeys.length === 1) {
primaryKeysCriteria[primaryKeys[0]] = primaryKeyValues
} else {
primaryKeysCriteria = (primaryKeyValues as string[] | object[]).map(
(primaryKeyValue) => ({
$and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })),
})
)
}
const queryOptions = buildQuery<TEntity>(primaryKeysCriteria, config)
const entities = await this[propertyRepositoryName].find(
queryOptions,
sharedContext
)
if (!entities?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`${model.name} with ${primaryKeys.join(", ")}: ${
Array.isArray(primaryKeyValues)
? primaryKeyValues.map((v) =>
[isString(v) ? v : Object.values(v)].join(", ")
)
: primaryKeyValues
} was not found`
)
}
return entities[0]
}
@InjectManager(propertyRepositoryName)
async list<TEntityMethod = TEntity>(
filters: TFilters["list"] = {},
config: FindConfig<TEntityMethod> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = buildQuery<TEntity>(filters, config)
return (await this[propertyRepositoryName].find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager(propertyRepositoryName)
async listAndCount<TEntityMethod = TEntity>(
filters: TFilters["listAndCount"] = {},
config: FindConfig<TEntityMethod> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = buildQuery<TEntity>(filters, config)
return (await this[propertyRepositoryName].findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
async create(
data: TDTOs["create"][],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this[propertyRepositoryName].create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
async update(
data: TDTOs["update"][],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this[propertyRepositoryName].update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, propertyRepositoryName)
async delete(
primaryKeyValues: string[] | object[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this[propertyRepositoryName].delete(primaryKeyValues, sharedContext)
}
@InjectTransactionManager(propertyRepositoryName)
async softDelete(
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], Record<string, unknown[]>]> {
return await this[propertyRepositoryName].softDelete(
idsOrFilter,
sharedContext
)
}
@InjectTransactionManager(propertyRepositoryName)
async restore(
idsOrFilter: string[] | InternalFilterQuery,
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], Record<string, unknown[]>]> {
return await this[propertyRepositoryName].restore(
idsOrFilter,
sharedContext
)
}
}
return AbstractService_ as unknown as new <TEntity extends {}>(
container: TContainer
) => AbstractService<TEntity, TContainer, TDTOs, TFilters>
}

View File

@@ -1,7 +1,7 @@
export * from "./load-module-database-config"
export * from "./decorators"
export * from "./build-query"
export * from "./retrieve-entity"
export * from "./loaders/mikro-orm-connection-loader"
export * from "./create-pg-connection"
export * from "./migration-scripts"
export * from "./abstract-service-factory"

View File

@@ -1,53 +0,0 @@
import { Context, DAL, FindConfig } from "@medusajs/types"
import {
MedusaError,
isDefined,
lowerCaseFirst,
upperCaseFirst,
} from "../common"
import { buildQuery } from "./build-query"
type RetrieveEntityParams<TDTO> = {
id: string
identifierColumn?: string
entityName: string
repository: DAL.TreeRepositoryService | DAL.RepositoryService
config: FindConfig<TDTO>
sharedContext?: Context
}
export async function retrieveEntity<TEntity, TDTO>({
id,
identifierColumn = "id",
entityName,
repository,
config = {},
sharedContext,
}: RetrieveEntityParams<TDTO>): Promise<TEntity> {
if (!isDefined(id)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"${lowerCaseFirst(entityName)}${upperCaseFirst(
identifierColumn
)}" must be defined`
)
}
const queryOptions = buildQuery<TEntity>(
{
[identifierColumn]: id,
},
config
)
const entities = await repository.find(queryOptions, sharedContext)
if (!entities?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`${entityName} with ${identifierColumn}: ${id} was not found`
)
}
return entities[0]
}