Chore/rm main entity concept (#7709)

**What**
Update the `MedusaService` class, factory and types to remove the concept of main modules. The idea being that all method will be explicitly named and suffixes to represent the object you are trying to manipulate.
This pr also includes various fixes in different modules

Co-authored-by: Stevche Radevski <4820812+sradevski@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-06-19 15:02:16 +02:00
committed by GitHub
parent 2895ccfba8
commit 48963f55ef
533 changed files with 6469 additions and 9769 deletions

View File

@@ -0,0 +1,282 @@
import { defineJoinerConfig } from "../joiner-config-builder"
import { Modules } from "../definition"
const FulfillmentSet = {
name: "FulfillmentSet",
}
const ShippingOption = {
name: "ShippingOption",
}
const ShippingProfile = {
name: "ShippingProfile",
}
const Fulfillment = {
name: "Fulfillment",
}
const FulfillmentProvider = {
name: "FulfillmentProvider",
}
const ServiceZone = {
name: "ServiceZone",
}
const GeoZone = {
name: "GeoZone",
}
const ShippingOptionRule = {
name: "ShippingOptionRule",
}
describe("defineJoiner", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases and models", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases without method suffix", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
})
})

View File

@@ -45,35 +45,22 @@ describe("Abstract Module Service Factory", () => {
class OtherModelMock1 {}
class OtherModelMock2 {}
const abstractModuleService = MedusaService<
const medusaService = MedusaService({
MainModelMock,
{
OtherModelMock1: {
dto: any
singular: "OtherModelMock1"
plural: "OtherModelMock1s"
}
OtherModelMock2: {
dto: any
singular: "OtherModelMock2"
plural: "OtherModelMock2s"
}
}
>(MainModelMock, {
OtherModelMock1,
OtherModelMock2,
})
describe("Main Model Methods", () => {
let instance
let instance: medusaService
beforeEach(() => {
jest.clearAllMocks()
instance = new abstractModuleService(containerMock)
instance = new medusaService(containerMock)
})
it("should have retrieve method", async () => {
const result = await instance.retrieve("1")
const result = await instance.retrieveMainModelMock("1")
expect(result).toEqual({ id: "1", name: "Item" })
expect(containerMock.mainModelMockService.retrieve).toHaveBeenCalledWith(
"1",
@@ -83,7 +70,7 @@ describe("Abstract Module Service Factory", () => {
})
it("should have list method", async () => {
const result = await instance.list()
const result = await instance.listMainModelMocks()
expect(result).toEqual([{ id: "1", name: "Item" }])
expect(containerMock.mainModelMockService.list).toHaveBeenCalledWith(
{},
@@ -93,7 +80,7 @@ describe("Abstract Module Service Factory", () => {
})
it("should have delete method", async () => {
await instance.delete("1")
await instance.deleteMainModelMocks("1")
expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith(
["1"],
defaultTransactionContext
@@ -101,7 +88,7 @@ describe("Abstract Module Service Factory", () => {
})
it("should have softDelete method", async () => {
const result = await instance.softDelete("1")
const result = await instance.softDeleteMainModelMocks("1")
expect(result).toEqual({})
expect(
containerMock.mainModelMockService.softDelete
@@ -109,7 +96,7 @@ describe("Abstract Module Service Factory", () => {
})
it("should have restore method", async () => {
const result = await instance.restore("1")
const result = await instance.restoreMainModelMocks("1")
expect(result).toEqual({})
expect(containerMock.mainModelMockService.restore).toHaveBeenCalledWith(
["1"],
@@ -118,7 +105,7 @@ describe("Abstract Module Service Factory", () => {
})
it("should have delete method with selector", async () => {
await instance.delete({ selector: { id: "1" } })
await instance.deleteMainModelMocks({ selector: { id: "1" } })
expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith(
[{ selector: { id: "1" } }],
defaultTransactionContext
@@ -131,7 +118,7 @@ describe("Abstract Module Service Factory", () => {
beforeEach(() => {
jest.clearAllMocks()
instance = new abstractModuleService(containerMock)
instance = new medusaService(containerMock)
})
it("should have retrieve method for other models", async () => {

View File

@@ -8,7 +8,7 @@ import type {
IEventBusModuleService,
IFileModuleService,
IFulfillmentModuleService,
IInventoryServiceNext,
IInventoryService,
INotificationModuleService,
IOrderModuleService,
IPaymentModuleService,
@@ -17,7 +17,7 @@ import type {
IPromotionModuleService,
IRegionModuleService,
ISalesChannelModuleService,
IStockLocationServiceNext,
IStockLocationService,
IStoreModuleService,
ITaxModuleService,
IUserModuleService,
@@ -84,7 +84,7 @@ declare module "@medusajs/types" {
[ModuleRegistrationName.CART]: ICartModuleService
[ModuleRegistrationName.CUSTOMER]: ICustomerModuleService
[ModuleRegistrationName.EVENT_BUS]: IEventBusModuleService
[ModuleRegistrationName.INVENTORY]: IInventoryServiceNext
[ModuleRegistrationName.INVENTORY]: IInventoryService
[ModuleRegistrationName.PAYMENT]: IPaymentModuleService
[ModuleRegistrationName.PRICING]: IPricingModuleService
[ModuleRegistrationName.PRODUCT]: IProductModuleService
@@ -92,7 +92,7 @@ declare module "@medusajs/types" {
[ModuleRegistrationName.SALES_CHANNEL]: ISalesChannelModuleService
[ModuleRegistrationName.TAX]: ITaxModuleService
[ModuleRegistrationName.FULFILLMENT]: IFulfillmentModuleService
[ModuleRegistrationName.STOCK_LOCATION]: IStockLocationServiceNext
[ModuleRegistrationName.STOCK_LOCATION]: IStockLocationService
[ModuleRegistrationName.USER]: IUserModuleService
[ModuleRegistrationName.WORKFLOW_ENGINE]: IWorkflowEngineService
[ModuleRegistrationName.REGION]: IRegionModuleService

View File

@@ -10,3 +10,4 @@ export * from "./medusa-internal-service"
export * from "./medusa-service"
export * from "./definition"
export * from "./event-builder-factory"
export * from "./joiner-config-builder"

View File

@@ -0,0 +1,147 @@
import { ModuleJoinerConfig } from "@medusajs/types"
import {
camelToSnakeCase,
deduplicate,
getCallerFilePath,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
import { join } from "path"
import { readdirSync, statSync } from "fs"
/**
* Define joiner config for a module based on the models (object representation or entities) present in the models directory. This action will be sync until
* we move to at least es2022 to have access to top-leve await.
*
* The aliases will be built from the entityQueryingConfig and custom aliases if provided, in case of aliases provided if the methodSuffix is not provided
* then it will be inferred from the entity name of the alias args.
*
* @param moduleName
* @param alias
* @param schema
* @param entityQueryingConfig
* @param linkableKeys
* @param primaryKeys
*/
export function defineJoinerConfig(
moduleName: string,
{
alias,
schema,
entityQueryingConfig,
linkableKeys,
primaryKeys,
}: {
alias?: ModuleJoinerConfig["alias"]
schema?: string
entityQueryingConfig?: { name: string }[]
linkableKeys?: Record<string, string>
primaryKeys?: string[]
} = {}
): Omit<
ModuleJoinerConfig,
"serviceName" | "primaryKeys" | "linkableKeys" | "alias"
> &
Required<
Pick<
ModuleJoinerConfig,
"serviceName" | "primaryKeys" | "linkableKeys" | "alias"
>
> {
let basePath = getCallerFilePath()
basePath = basePath.includes("dist")
? basePath.split("dist")[0] + "dist"
: basePath.split("src")[0] + "src"
basePath = join(basePath, "models")
const models = deduplicate(
[...(entityQueryingConfig ?? loadModels(basePath))].flatMap((v) => v!.name)
).map((name) => ({ name }))
return {
serviceName: moduleName,
primaryKeys: primaryKeys ?? ["id"],
schema,
linkableKeys:
linkableKeys ??
models.reduce((acc, entity) => {
acc[`${camelToSnakeCase(entity.name).toLowerCase()}_id`] = entity.name
return acc
}, {} as Record<string, string>),
alias: [
...[...(alias ?? ([] as any))].map((alias) => ({
name: alias.name,
args: {
entity: alias.args.entity,
methodSuffix:
alias.args.methodSuffix ??
pluralize(upperCaseFirst(alias.args.entity)),
},
})),
...models.map((entity, i) => ({
name: [
`${camelToSnakeCase(entity.name).toLowerCase()}`,
`${pluralize(camelToSnakeCase(entity.name).toLowerCase())}`,
],
args: {
entity: entity.name,
methodSuffix: pluralize(upperCaseFirst(entity.name)),
},
})),
],
}
}
/**
* Build entities name to linkable keys map
* @param linkableKeys
*/
export function buildEntitiesNameToLinkableKeysMap(
linkableKeys: Record<string, string>
): MapToConfig {
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(linkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
return entityLinkableKeysMap
}
function loadModels(basePath: string) {
const excludedExtensions = [".ts.map", ".js.map", ".d.ts"]
let modelsFiles: any[] = []
try {
modelsFiles = readdirSync(basePath)
} catch (e) {}
return modelsFiles
.flatMap((file) => {
if (
file.startsWith("index.") ||
excludedExtensions.some((ext) => file.endsWith(ext))
) {
return
}
const filePath = join(basePath, file)
const stats = statSync(filePath)
if (stats.isFile()) {
try {
const required = require(filePath)
return Object.values(required).filter(
(resource) => typeof resource === "function" && !!resource.name
)
} catch (e) {}
}
return
})
.filter(Boolean) as { name: string }[]
}

View File

@@ -50,93 +50,60 @@ type ModelDTOConfig = {
update?: any
/**
* @internal
* @deprecated
*/
singular?: string
/**
* @internal
* @deprecated
*/
plural?: string
}
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>
> = {
type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
[Key in keyof T as `${Capitalize<Key & string>}`]: {
dto: ModelConfigurationToDto<T[Key]>
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
create: any
update: any
singular: T[Key] extends { singular: string } ? T[Key]["singular"] : string
plural: T[Key] extends { plural: string } ? T[Key]["plural"] : string
}
}
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
*/
type ExtractSingularName<T extends Record<any, any>, K = keyof T> = Capitalize<
T[K] extends { singular?: string } ? T[K]["singular"] & string : K & string
>
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
* The pluralize will move to where it should be used instead
*/
type ExtractPluralName<T extends Record<any, any>, K = keyof T> = T[K] extends {
plural?: string
}
? T[K]["plural"] & string
: Pluralize<K & string>
// TODO: this will be removed in the follow up pr once the main entity concept will be removed
type ModelConfiguration = Constructor<any> | ModelDTOConfig | any
// TODO: The future expected entry will be a DML object but in the meantime we have to maintain backward compatibility for ouw own modules and therefore we need to support Constructor<any> as well as this temporary object
type TEntityEntries<Keys = string> = Record<
Keys & string,
Constructor<any> | { name?: string; singular?: string; plural?: string }
>
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<TEntryEntityConfig>
list(
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TEntryEntityConfig[]>
listAndCount(
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<[TEntryEntityConfig[], number]>
delete(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
softDelete<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
restore<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
type ExtractKeysFromConfig<EntitiesConfig> = EntitiesConfig extends {
__empty: any
}
? string
: keyof EntitiesConfig
export type AbstractModuleService<
TEntryEntityConfig extends ModelConfiguration,
TEntitiesDtoConfig extends EntitiesConfigTemplate
> = AbstractModuleServiceBase<TEntryEntityConfig> & {
> = {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
@@ -269,27 +236,21 @@ export type AbstractModuleService<
* @internal
*/
function buildMethodNamesFromModel(
model: ModelConfiguration,
suffixed: boolean = true
modelName: string,
model: TEntityEntries[keyof TEntityEntries]
): Record<string, string> {
return methods.reduce((acc, method) => {
let modelName: string = ""
let normalizedModelName: string = ""
if (method === "retrieve") {
modelName =
"singular" in model && model.singular
? model.singular
: (model as { name: string }).name
normalizedModelName =
"singular" in model && model.singular ? model.singular : modelName
} else {
modelName =
"plural" in model && model.plural
? model.plural
: pluralize((model as { name: string }).name)
normalizedModelName =
"plural" in model && model.plural ? model.plural : pluralize(modelName)
}
const methodName = suffixed
? `${method}${upperCaseFirst(modelName)}`
: method
const methodName = `${method}${upperCaseFirst(normalizedModelName)}`
return { ...acc, [method]: methodName }
}, {})
@@ -300,31 +261,6 @@ function buildMethodNamesFromModel(
*
* @example
*
* const entities = {
* Currency,
* Price,
* PriceList,
* PriceListRule,
* PriceListRuleValue,
* PriceRule,
* PriceSetRuleType,
* RuleType,
* }
*
* class MyService extends ModulesSdkUtils.MedusaService<
* PricingTypes.PriceSetDTO,
* {
* Currency: { dto: PricingTypes.CurrencyDTO }
* Price: { dto: PricingTypes.PriceDTO }
* PriceRule: { dto: PricingTypes.PriceRuleDTO }
* RuleType: { dto: PricingTypes.RuleTypeDTO }
* PriceList: { dto: PricingTypes.PriceListDTO }
* PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
* }
* >(PriceSet, entities, entityNameToLinkableKeysMap) {}
*
* @example
*
* // Here the DTO's and names will be inferred from the arguments
*
* const entities = {
@@ -338,26 +274,21 @@ function buildMethodNamesFromModel(
* RuleType,
* }
*
* class MyService extends ModulesSdkUtils.MedusaService(PriceSet, entities, entityNameToLinkableKeysMap) {}
* class MyService extends ModulesSdkUtils.MedusaService(entities, entityNameToLinkableKeysMap) {}
*
* @param entryEntity
* @param entities
* @param entityNameToLinkableKeysMap
*/
export function MedusaService<
TEntryEntityConfig extends ModelConfiguration = ModelConfiguration,
EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
TEntities extends Record<string, ModelConfiguration> = Record<
string,
ModelConfiguration
>
TEntities extends TEntityEntries<
ExtractKeysFromConfig<EntitiesConfig>
> = TEntityEntries<ExtractKeysFromConfig<EntitiesConfig>>
>(
entryEntity: (TEntryEntityConfig & { name: string }) | Constructor<any>,
entities: TEntities,
entityNameToLinkableKeysMap: MapToConfig = {}
): {
new (...args: any[]): AbstractModuleService<
ModelConfigurationToDto<TEntryEntityConfig>,
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
@@ -595,7 +526,6 @@ export function MedusaService<
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 === ModuleRegistrationName.EVENT_BUS
)
@@ -618,33 +548,18 @@ export function MedusaService<
}
}
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(entryEntityMethods)) {
buildAndAssignMethodImpl(
AbstractModuleService_.prototype,
method,
methodName,
entryEntity.name
)
}
/**
* Build the retrieve/list/listAndCount/delete/softDelete/restore methods for all the other models
*/
const entitiesMethods: [
string,
ModelConfiguration,
TEntities[keyof TEntities],
Record<string, string>
][] = Object.entries(entities).map(([name, config]) => [
name,
config,
buildMethodNamesFromModel(config),
config as TEntities[keyof TEntities],
buildMethodNamesFromModel(name, config as TEntities[keyof TEntities]),
])
for (let [modelName, model, modelsMethods] of entitiesMethods) {