feat: refactor module joiner config and links generation (#7859)

* feat: refactor module joiner config and links generation

* improve typings

* WIP

* WIP

* WIP

* rename type file

* create link config

* finish typings and add utils

* improve links

* WIP typings

* finalize ExportModule utils

* finalize ExportModule utils

* fix: dml tests

* improve and fixes

* simplify typings with id changes

* add toJSON

* multiple fixes and entity builder fixes

* fix currency searchable

* fix tests

* medusa service refactoring

* cleanup

* cleanup and fixes

* make module name optional

* renaming

---------

Co-authored-by: Harminder Virk <virk.officials@gmail.com>
This commit is contained in:
Adrien de Peretti
2024-07-03 13:12:49 +02:00
committed by GitHub
parent 5aa62e59e4
commit 617a5972bf
89 changed files with 1706 additions and 950 deletions

View File

@@ -0,0 +1,53 @@
import { model } from "../../../dml"
export const FulfillmentSet = {
name: "FulfillmentSet",
}
export const ShippingOption = {
name: "ShippingOption",
}
export const ShippingProfile = {
name: "ShippingProfile",
}
export const Fulfillment = {
name: "Fulfillment",
}
export const FulfillmentProvider = {
name: "FulfillmentProvider",
}
export const ServiceZone = {
name: "ServiceZone",
}
export const GeoZone = {
name: "GeoZone",
}
export const ShippingOptionRule = {
name: "ShippingOptionRule",
}
export const dmlFulfillmentSet = model.define(FulfillmentSet.name, {
id: model.id().primaryKey(),
})
export const dmlShippingOption = model.define(ShippingOption.name, {
id: model.id().primaryKey(),
})
export const dmlShippingProfile = model.define(ShippingProfile.name, {
id: model.id().primaryKey(),
})
export const dmlFulfillment = model.define(Fulfillment.name, {
id: model.id().primaryKey(),
})
export const dmlFulfillmentProvider = model.define(FulfillmentProvider.name, {
id: model.id().primaryKey(),
})
export const dmlServiceZone = model.define(ServiceZone.name, {
id: model.id().primaryKey(),
})
export const dmlGeoZone = model.define(GeoZone.name, {
id: model.id().primaryKey(),
})
export const dmlShippingOptionRule = model.define(ShippingOptionRule.name, {
id: model.id().primaryKey(),
})

View File

@@ -1,315 +1,505 @@
import { defineJoinerConfig } from "../joiner-config-builder"
import {
buildLinkableKeysFromDmlObjects,
buildLinkableKeysFromMikroOrmObjects,
buildLinkConfigFromDmlObjects,
defineJoinerConfig,
} from "../joiner-config-builder"
import { Modules } from "../definition"
import { model } from "../../dml"
import { expectTypeOf } from "expect-type"
import {
dmlFulfillment,
dmlFulfillmentProvider,
dmlFulfillmentSet,
dmlGeoZone,
dmlServiceZone,
dmlShippingOption,
dmlShippingOptionRule,
dmlShippingProfile,
Fulfillment,
FulfillmentProvider,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingOptionRule,
ShippingProfile,
} from "../__fixtures__/joiner-config/entities"
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("joiner-config-builder", () => {
describe("defineJoiner | Mikro orm objects", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
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",
},
},
],
})
})
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",
it("should return a full joiner configuration with custom aliases", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
],
})
})
it("should return a full joiner configuration with custom aliases and models", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
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,
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
{
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",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases overriding defaults", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [FulfillmentSet],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
},
],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
})
})
it("should return a full joiner configuration with custom aliases", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
describe("defineJoiner | DML objects", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
dmlFulfillmentSet,
dmlShippingOption,
dmlShippingProfile,
dmlFulfillment,
dmlFulfillmentProvider,
dmlServiceZone,
dmlGeoZone,
dmlShippingOptionRule,
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
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: ["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 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",
},
},
],
})
describe("buildLinkableKeysFromDmlObjects", () => {
it("should return a linkableKeys object based on the DML's primary keys", () => {
const user = model.define("user", {
id: model.id().primaryKey(),
name: model.text(),
})
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",
},
},
],
const car = model.define("car", {
id: model.id(),
number_plate: model.text().primaryKey(),
test: model.text(),
})
const linkableKeys = buildLinkableKeysFromDmlObjects([user, car])
expectTypeOf(linkableKeys).toMatchTypeOf<{
user_id: "User"
car_number_plate: "Car"
}>()
expect(linkableKeys).toEqual({
user_id: user.name,
car_number_plate: car.name,
})
})
})
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",
},
},
],
})
describe("buildLinkableKeysFromMikroOrmObjects", () => {
it("should return a linkableKeys object based on the mikro orm models name", () => {
class User {}
class Car {}
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
const linkableKeys = buildLinkableKeysFromMikroOrmObjects([Car, User])
expect(linkableKeys).toEqual({
user_id: User.name,
car_id: Car.name,
})
})
})
it("should return a full joiner configuration with custom aliases overriding defaults", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [FulfillmentSet],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
describe("buildLinkConfigFromDmlObjects", () => {
it("should return a link config object based on the DML's primary keys", () => {
const user = model.define("user", {
id: model.id().primaryKey(),
name: model.text(),
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
},
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
const car = model.define("car", {
id: model.id(),
number_plate: model.text().primaryKey(),
})
const linkConfig = buildLinkConfigFromDmlObjects([user, car])
expectTypeOf(linkConfig).toMatchTypeOf<{
user: {
id: {
linkable: "user_id"
primaryKey: "id"
}
toJSON: () => {
linkable: string
primaryKey: string
}
}
car: {
number_plate: {
linkable: "car_number_plate"
primaryKey: "number_plate"
}
toJSON: () => {
linkable: string
primaryKey: string
}
}
}>()
expect(linkConfig.user.id).toEqual({
linkable: "user_id",
primaryKey: "id",
})
expect(linkConfig.car.number_plate).toEqual({
linkable: "car_number_plate",
primaryKey: "number_plate",
})
expect(linkConfig.car.toJSON()).toEqual({
linkable: "car_number_plate",
primaryKey: "number_plate",
})
expect(linkConfig.user.toJSON()).toEqual({
linkable: "user_id",
primaryKey: "id",
})
})
})
})

View File

@@ -1,6 +1,9 @@
import { Context, EventBusTypes } from "@medusajs/types"
// TODO should that move closer to the event bus? and maybe be rename to moduleEventBuilderFactory
/**
*
* Factory function to create event builders for different entities
*
* @example
@@ -48,7 +51,8 @@ export function eventBuilderFactory({
// The event enums contains event formatted like so [object]_[action] e.g. PRODUCT_CREATED
// We expect the keys of events to be fully uppercased
const eventName = eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
const eventName =
eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
data.forEach((dataItem) => {
messages.push({

View File

@@ -13,4 +13,4 @@ export * from "./medusa-internal-service"
export * from "./medusa-service"
export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder"
export * from "./module"

View File

@@ -1,14 +1,24 @@
import { JoinerServiceConfigAlias, ModuleJoinerConfig } from "@medusajs/types"
import {
JoinerServiceConfigAlias,
ModuleJoinerConfig,
PropertyType,
} from "@medusajs/types"
import { dirname, join } from "path"
import {
MapToConfig,
camelToSnakeCase,
deduplicate,
getCallerFilePath,
isObject,
lowerCaseFirst,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
import { loadModels } from "./loaders/load-models"
import { DmlEntity } from "../dml"
import { BaseRelationship } from "../dml/relations/base"
import { PrimaryKeyModifier } from "../dml/properties/primary-key"
import { InferLinkableKeys, InfersLinksConfig } from "./types/links-config"
/**
* 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
@@ -20,7 +30,7 @@ import { loadModels } from "./loaders/load-models"
* @param moduleName
* @param alias
* @param schema
* @param entityQueryingConfig
* @param models
* @param linkableKeys
* @param primaryKeys
*/
@@ -29,13 +39,13 @@ export function defineJoinerConfig(
{
alias,
schema,
entityQueryingConfig,
models,
linkableKeys,
primaryKeys,
}: {
alias?: JoinerServiceConfigAlias[]
schema?: string
entityQueryingConfig?: { name: string }[]
models?: DmlEntity<any, any>[] | { name: string }[]
linkableKeys?: Record<string, string>
primaryKeys?: string[]
} = {}
@@ -62,20 +72,64 @@ export function defineJoinerConfig(
basePath = join(basePath, "models")
const models = deduplicate(
[...(entityQueryingConfig ?? loadModels(basePath))].flatMap((v) => v!.name)
).map((name) => ({ name }))
let loadedModels = models ?? loadModels(basePath)
const modelDefinitions = new Map(
loadedModels
.filter((model) => !!DmlEntity.isDmlEntity(model))
.map((model) => [model.name, model])
)
const mikroOrmObjects = new Map(
loadedModels
.filter((model) => !DmlEntity.isDmlEntity(model))
.map((model) => [model.name, model])
)
// We prioritize DML if there is any equivalent Mikro orm entities found
loadedModels = [...modelDefinitions.values()]
mikroOrmObjects.forEach((model) => {
if (modelDefinitions.has(model.name)) {
return
}
loadedModels.push(model)
})
if (!linkableKeys) {
const linkableKeysFromDml = buildLinkableKeysFromDmlObjects([
...modelDefinitions.values(),
])
const linkableKeysFromMikroOrm = buildLinkableKeysFromMikroOrmObjects([
...mikroOrmObjects.values(),
])
linkableKeys = {
...linkableKeysFromDml,
...linkableKeysFromMikroOrm,
}
}
if (!primaryKeys && modelDefinitions.size) {
const linkConfig = buildLinkConfigFromDmlObjects([
...modelDefinitions.values(),
])
primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig) as any[])
.filter((linkableConfig) => isObject(linkableConfig))
.map((linkableConfig) => {
return linkableConfig.primaryKey
})
})
)
}
// TODO: In the context of DML add a validation on primary keys and linkable keys if the consumer provide them manually. follow up pr
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>),
linkableKeys: linkableKeys,
alias: [
...[...(alias ?? ([] as any))].map((alias) => ({
name: alias.name,
@@ -86,7 +140,7 @@ export function defineJoinerConfig(
pluralize(upperCaseFirst(alias.args.entity)),
},
})),
...models
...loadedModels
.filter((model) => {
return (
!alias || !alias.some((alias) => alias.args?.entity === model.name)
@@ -106,8 +160,153 @@ export function defineJoinerConfig(
}
}
/**
* From a set of DML objects, build the linkable keys
*
* @example
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* // output:
* // {
* // user_id: 'User',
* // car_number_plate: 'Car',
* // }
*
* @param models
*/
export function buildLinkableKeysFromDmlObjects<
const T extends DmlEntity<any, any>[],
LinkableKeys = InferLinkableKeys<T>
>(models: T): LinkableKeys {
const linkableKeys = {} as LinkableKeys
for (const model of models) {
if (!DmlEntity.isDmlEntity(model)) {
continue
}
const schema = model.schema
const primaryKeys: string[] = []
for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) {
continue
}
const parsedProperty = (value as PropertyType<any>).parse(property)
if (PrimaryKeyModifier.isPrimaryKeyModifier(value)) {
const linkableKeyName =
parsedProperty.dataType.options?.linkable ??
`${camelToSnakeCase(model.name).toLowerCase()}_${property}`
primaryKeys.push(linkableKeyName)
}
}
if (primaryKeys.length) {
primaryKeys.forEach((primaryKey) => {
linkableKeys[primaryKey] = model.name
})
}
}
return linkableKeys
}
/**
* Build linkable keys from MikroORM objects
* @deprecated
* @param models
*/
export function buildLinkableKeysFromMikroOrmObjects(
models: Function[]
): Record<string, string> {
return models.reduce((acc, entity) => {
acc[`${camelToSnakeCase(entity.name).toLowerCase()}_id`] = entity.name
return acc
}, {}) as Record<string, string>
}
/**
* Build entities name to linkable keys map
*
* @example
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* // output:
* // {
* // toJSON: function () { },
* // user: {
* // id: "user_id",
* // },
* // car: {
* // number_plate: "car_number_plate",
* // },
* // }
*
* @param models
*/
export function buildLinkConfigFromDmlObjects<
const T extends DmlEntity<any, any>[]
>(models: T = [] as unknown as T): InfersLinksConfig<T> {
const linkConfig = {} as InfersLinksConfig<T>
for (const model of models) {
if (!DmlEntity.isDmlEntity(model)) {
continue
}
const schema = model.schema
const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= {
toJSON: function () {
const linkables = Object.entries(this)
.filter(([name]) => name !== "toJSON")
.map(([, object]) => object)
const lastIndex = linkables.length - 1
return linkables[lastIndex]
},
})
for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) {
continue
}
const parsedProperty = (value as PropertyType<any>).parse(property)
if (PrimaryKeyModifier.isPrimaryKeyModifier(value)) {
const linkableKeyName =
parsedProperty.dataType.options?.linkable ??
`${camelToSnakeCase(model.name).toLowerCase()}_${property}`
modelLinkConfig[property] = {
linkable: linkableKeyName,
primaryKey: property,
}
}
}
}
return linkConfig as InfersLinksConfig<T> & Record<any, any>
}
/**
* Reversed map from linkableKeys to entity name to linkable keys
* @param linkableKeys
*/
export function buildEntitiesNameToLinkableKeysMap(

View File

@@ -27,15 +27,15 @@ export function loadModels(basePath: string) {
if (stats.isFile()) {
try {
const required = require(filePath) as {
[key: string]: { name?: string }
}
const required = require(filePath)
return Object.values(required).filter((resource) => !!resource.name)
return Object.values(required).filter(
(resource: any) => !!resource.name
)
} catch (e) {}
}
return
})
.filter(Boolean) as { name: string }[]
.filter(Boolean) as any[]
}

View File

@@ -35,12 +35,16 @@ type SelectorAndData = {
data: any
}
export function MedusaInternalService<TContainer extends object = object>(
export function MedusaInternalService<
TContainer extends object = object,
TEntity extends object = any
>(
rawModel: any
): {
new <TEntity extends object = any>(
container: TContainer
): ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
new (container: TContainer): ModulesSdkTypes.IMedusaInternalService<
TEntity,
TContainer
>
} {
const model = DmlEntity.isDmlEntity(rawModel)
? toMikroORMEntity(rawModel)
@@ -49,7 +53,7 @@ export function MedusaInternalService<TContainer extends object = object>(
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
const propertyRepositoryName = `__${injectedRepositoryName}__`
class AbstractService_<TEntity extends object>
class AbstractService_
implements ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
{
readonly __container__: TContainer;
@@ -556,7 +560,5 @@ export function MedusaInternalService<TContainer extends object = object>(
}
}
return AbstractService_ as unknown as new <TEntity extends {}>(
container: TContainer
) => ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
return AbstractService_ as any
}

View File

@@ -2,11 +2,10 @@
* Utility factory and interfaces for module service public facing API
*/
import {
Constructor,
Context,
FindConfig,
IEventBusModuleService,
Pluralize,
ModuleJoinerConfig,
RepositoryService,
RestoreReturn,
SoftDeleteReturn,
@@ -22,17 +21,15 @@ import {
} from "../common"
import { InjectManager, MedusaContext } from "./decorators"
import { ModuleRegistrationName } from "./definition"
import { DmlEntity } from "../dml"
type BaseMethods =
| "retrieve"
| "list"
| "listAndCount"
| "delete"
| "softDelete"
| "restore"
| "create"
| "update"
import {
BaseMethods,
EntitiesConfigTemplate,
ExtractKeysFromConfig,
MedusaServiceReturnType,
ModelConfigurationsToConfigTemplate,
TEntityEntries,
} from "./types/medusa-service"
import { buildEntitiesNameToLinkableKeysMap } from "./joiner-config-builder"
const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[]
const writeMethods = [
@@ -45,200 +42,6 @@ const writeMethods = [
const methods: BaseMethods[] = [...readMethods, ...writeMethods]
type ModelDTOConfig = {
dto: object
create?: any
update?: any
/**
* @internal
* @deprecated
*/
singular?: string
/**
* @internal
* @deprecated
*/
plural?: string
}
type EntitiesConfigTemplate = { [key: string]: ModelDTOConfig }
type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
[Key in keyof T as `${Capitalize<Key & string>}`]: {
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
create: any
update: any
singular: T[Key] extends { singular: string } ? T[Key]["singular"] : Key
plural: T[Key] extends { plural: string }
? T[Key]["plural"]
: Pluralize<Key & 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> = Capitalize<
T[K] extends {
plural?: string
}
? T[K]["plural"] & string
: Pluralize<K & string>
>
// 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>
| DmlEntity<any>
| { name?: string; singular?: string; plural?: string }
>
type ExtractKeysFromConfig<EntitiesConfig> = EntitiesConfig extends {
__empty: any
}
? string
: keyof EntitiesConfig
export type AbstractModuleService<
TEntitiesDtoConfig extends Record<string, any>
> = {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
id: string,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `list${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `listAndCount${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
[TEntitiesDtoConfig[TEntityName]["dto"][], number]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `delete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `softDelete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `restore${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
} & {
[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"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any, sharedContext?: Context): 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
>}`]: {
(
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
*/
@@ -263,6 +66,36 @@ function buildMethodNamesFromModel(
}, {})
}
/**
* Accessible from the MedusaService, holds the model objects when provided
*/
export const MedusaServiceModelObjectsSymbol = Symbol.for(
"MedusaServiceModelObjectsSymbol"
)
/**
* Symbol to mark a class as a Medusa service
*/
export const MedusaServiceSymbol = Symbol.for("MedusaServiceSymbol")
/**
* Accessible from the MedusaService, holds the entity name to linkable keys map
* to be used for softDelete and restore methods
*/
export const MedusaServiceEntityNameToLinkableKeysMapSymbol = Symbol.for(
"MedusaServiceEntityNameToLinkableKeysMapSymbol"
)
/**
* Check if a value is a Medusa service
* @param value
*/
export function isMedusaService(
value: any
): value is MedusaServiceReturnType<any> {
return value && value?.prototype[MedusaServiceSymbol]
}
/**
* Factory function for creating an abstract module service
*
@@ -281,26 +114,22 @@ function buildMethodNamesFromModel(
* RuleType,
* }
*
* class MyService extends ModulesSdkUtils.MedusaService(entities, entityNameToLinkableKeysMap) {}
* class MyService extends ModulesSdkUtils.MedusaService(entities) {}
*
* @param entities
* @param entityNameToLinkableKeysMap
*/
export function MedusaService<
EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
TEntities extends TEntityEntries<
const EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
const TEntities extends TEntityEntries<
ExtractKeysFromConfig<EntitiesConfig>
> = TEntityEntries<ExtractKeysFromConfig<EntitiesConfig>>
>(
entities: TEntities,
entityNameToLinkableKeysMap: MapToConfig = {}
): {
new (...args: any[]): AbstractModuleService<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>
} {
entities: TEntities
): MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
> {
const buildAndAssignMethodImpl = function (
klassPrototype: any,
method: string,
@@ -474,7 +303,7 @@ export function MedusaService<
// eg: product.id = product_id, variant.id = variant_id
const mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
this[MedusaServiceEntityNameToLinkableKeysMapSymbol],
{
pick: config.returnLinkableKeys,
}
@@ -506,7 +335,7 @@ export function MedusaService<
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
this[MedusaServiceEntityNameToLinkableKeysMapSymbol],
{
pick: config.returnLinkableKeys,
}
@@ -522,11 +351,23 @@ export function MedusaService<
}
class AbstractModuleService_ {
[MedusaServiceSymbol] = true
static [MedusaServiceModelObjectsSymbol] = Object.values(
entities
) as unknown as MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>["$modelObjects"];
[MedusaServiceEntityNameToLinkableKeysMapSymbol]: MapToConfig
readonly __container__: Record<any, any>
readonly baseRepository_: RepositoryService
readonly eventBusModuleService_: IEventBusModuleService;
readonly eventBusModuleService_: IEventBusModuleService
[key: string]: any
__joinerConfig?(): ModuleJoinerConfig
constructor(container: Record<any, any>) {
this.__container__ = container
@@ -539,6 +380,11 @@ export function MedusaService<
this.eventBusModuleService_ = hasEventBusModuleService
? this.__container__.eventBusModuleService
: undefined
this[MedusaServiceEntityNameToLinkableKeysMapSymbol] =
buildEntitiesNameToLinkableKeysMap(
this.__joinerConfig?.()?.linkableKeys ?? {}
)
}
protected async emitEvents_(groupedEvents) {
@@ -563,7 +409,7 @@ export function MedusaService<
string,
TEntities[keyof TEntities],
Record<string, string>
][] = Object.entries(entities).map(([name, config]) => [
][] = Object.entries(entities as {}).map(([name, config]) => [
name,
config as TEntities[keyof TEntities],
buildMethodNamesFromModel(name, config as TEntities[keyof TEntities]),

View File

@@ -14,7 +14,7 @@ type Options = Partial<MikroORMOptions> & {
| EntityClass<AnyEntity>
| EntityClassGroup<AnyEntity>
| EntitySchema
| DmlEntity<any>
| DmlEntity<any, any>
)[]
databaseName: string
}

View File

@@ -0,0 +1,45 @@
import { Constructor, ModuleExports } from "@medusajs/types"
import { MedusaServiceModelObjectsSymbol } from "./medusa-service"
import {
buildLinkConfigFromDmlObjects,
defineJoinerConfig,
} from "./joiner-config-builder"
import { InfersLinksConfig } from "./types/links-config"
import { DmlEntity } from "../dml"
/**
* Wrapper to build the module export and auto generate the joiner config if needed as well as
* return a links object based on the DML objects
* @param moduleName
* @param service
* @param loaders
* @constructor
*/
export function Module<
const Service extends Constructor<any>,
const ModelObjects extends DmlEntity<any, any>[] = Service extends {
$modelObjects: infer $DmlObjects
}
? $DmlObjects
: [],
Links = keyof ModelObjects extends never
? Record<string, any>
: InfersLinksConfig<ModelObjects>
>({
name = "",
service,
loaders,
}: ModuleExports<Service> & { name?: string }): ModuleExports<Service> & {
links: Links
} {
service.prototype.__joinerConfig ??= defineJoinerConfig(name)
const dmlObjects = service[MedusaServiceModelObjectsSymbol]
return {
service,
loaders,
links: (dmlObjects?.length
? buildLinkConfigFromDmlObjects(dmlObjects)
: {}) as Links,
}
}

View File

@@ -0,0 +1,131 @@
import {
DMLSchema,
IDmlEntityConfig,
InferDmlEntityNameFromConfig,
SnakeCase,
} from "@medusajs/types"
import { DmlEntity } from "../../dml"
import { PrimaryKeyModifier } from "../../dml/properties/primary-key"
type FlattenUnion<T> = T extends { [K in keyof T]: infer U }
? { [K in keyof T]: U }
: never
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
type InferLinkableKeyName<
Key,
Property,
DmlConfig extends IDmlEntityConfig
> = Property extends PrimaryKeyModifier<any, any>
? `${Lowercase<SnakeCase<InferDmlEntityNameFromConfig<DmlConfig>>>}_${Key &
string}`
: never
type InferSchemaLinkableKeys<T> = T extends DmlEntity<
infer Schema,
infer Config
>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferLinkableKeyName<K, Schema[K], Config>
: never]: InferDmlEntityNameFromConfig<Config>
}
: {}
type InferSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
[K in keyof T]: InferSchemaLinkableKeys<T[K]>
}
type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
[K in keyof InferSchemasLinkableKeys<T>]: InferSchemasLinkableKeys<T>[K]
}
/**
* From an array of DmlEntity, returns a formatted object with the linkable keys
*
* @example:
*
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' }
*
*/
export type InferLinkableKeys<T extends DmlEntity<any, any>[]> =
UnionToIntersection<FlattenUnion<AggregateSchemasLinkableKeys<T>>[0]>
type InferPrimaryKeyNameOrNever<
Schema extends DMLSchema,
Key extends keyof Schema
> = Schema[Key] extends PrimaryKeyModifier<any, any> ? Key : never
type InferSchemaLinksConfig<T> = T extends DmlEntity<infer Schema, infer Config>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferPrimaryKeyNameOrNever<Schema, K>
: never]: {
linkable: InferLinkableKeyName<K, Schema[K], Config>
primaryKey: K
}
}
: {}
/**
* From an array of DmlEntity, returns a formatted object with the linkable keys
*
* @example:
*
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* const linkConfig = buildLinkConfigFromDmlObjects([user, car])
* // {
* // user: {
* // id: {
* // linkable: 'user_id',
* // primaryKey: 'id'
* // },
* // toJSON() { ... }
* // },
* // car: {
* // number_plate: {
* // linkable: 'car_number_plate',
* // primaryKey: 'number_plate'
* // },
* // toJSON() { ... }
* // }
* // }
*
*/
export type InfersLinksConfig<T extends DmlEntity<any, any>[]> =
UnionToIntersection<{
[K in keyof T as T[K] extends DmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>>
: never]: InferSchemaLinksConfig<T[K]> & {
toJSON: () => {
linkable: string
primaryKey: string
}
}
}>

View File

@@ -0,0 +1,261 @@
import {
Constructor,
Context,
FindConfig,
Pluralize,
RestoreReturn,
SoftDeleteReturn,
} from "@medusajs/types"
import { DmlEntity } from "../../dml"
export type BaseMethods =
| "retrieve"
| "list"
| "listAndCount"
| "delete"
| "softDelete"
| "restore"
| "create"
| "update"
export type ModelDTOConfig = {
dto: object
model?: DmlEntity<any, any>
create?: any
update?: any
/**
* @internal
* @deprecated
*/
singular?: string
/**
* @internal
* @deprecated
*/
plural?: string
}
export type EntitiesConfigTemplate = { [key: string]: ModelDTOConfig }
export type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
[Key in keyof T]: {
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
model: T[Key] extends { model: infer MODEL }
? MODEL
: T[Key] extends DmlEntity<any, any>
? T[Key]
: never
/**
* @deprecated
*/
create: any
update: any
/**
* @deprecated
*/
singular: T[Key] extends { singular: string } ? T[Key]["singular"] : Key
/**
* @deprecated
*/
plural: T[Key] extends { plural: string }
? T[Key]["plural"]
: Pluralize<Key & string>
}
}
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
*/
export 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
*/
export type ExtractPluralName<
T extends Record<any, any>,
K = keyof T
> = Capitalize<
T[K] extends {
plural?: string
}
? T[K]["plural"] & string
: Pluralize<K & string>
>
// TODO: The future expected entry will be a MODEL 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
export type TEntityEntries<Keys = string> = Record<
Keys & string,
| DmlEntity<any, any>
/**
* @deprecated
*/
| Constructor<any>
/**
* @deprecated
*/
| { name?: string; singular?: string; plural?: string }
>
export type ExtractKeysFromConfig<EntitiesConfig> = EntitiesConfig extends {
__empty: any
}
? string
: keyof EntitiesConfig
export type AbstractModuleService<
TEntitiesDtoConfig extends Record<string, any>
> = {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
id: string,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `list${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `listAndCount${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
[TEntitiesDtoConfig[TEntityName]["dto"][], number]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `delete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `softDelete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `restore${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
} & {
[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"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any, sharedContext?: Context): 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
>}`]: {
(
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"][]>
}
}*/
type InferModelFromConfig<T> = {
[K in keyof T as T[K] extends { model: any }
? K
: K extends DmlEntity<any, any>
? K
: never]: T[K] extends {
model: infer MODEL
}
? MODEL extends DmlEntity<any, any>
? MODEL
: never
: T[K] extends DmlEntity<any, any>
? T[K]
: never
}
export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = {
new (...args: any[]): AbstractModuleService<ModelsConfig>
$modelObjects: InferModelFromConfig<ModelsConfig>[keyof InferModelFromConfig<ModelsConfig>][]
}