diff --git a/integration-tests/modules/__tests__/link-modules/define-link.spec.ts b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts new file mode 100644 index 0000000000..a596010123 --- /dev/null +++ b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts @@ -0,0 +1,92 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import CurrencyModule from "@medusajs/currency" +import RegionModule from "@medusajs/region" +import { defineLink } from "@medusajs/utils" +import { MedusaModule } from "@medusajs/modules-sdk" + +jest.setTimeout(50000) + +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("defineLink", () => { + let appContainer + let remoteQuery + + beforeAll(async () => { + appContainer = getContainer() + remoteQuery = appContainer.resolve("remoteQuery") + }) + + it("should generate a proper link definition", async () => { + const currencyLinks = CurrencyModule.linkable + const regionLinks = RegionModule.linkable + + const link = defineLink(currencyLinks.currency, regionLinks.region) + + const linkDefinition = MedusaModule.getCustomLinks() + .map((linkDefinition: any) => { + const definition = linkDefinition(MedusaModule.getLoadedModules()) + return definition.serviceName === link.serviceName && definition + }) + .filter(Boolean)[0] + + expect(link.serviceName).toEqual("currencyCurrencyRegionRegionLink") + expect(linkDefinition).toEqual({ + serviceName: "currencyCurrencyRegionRegionLink", + isLink: true, + alias: [ + { + name: ["currency_region"], + args: { + entity: "LinkCurrencyCurrencyRegionRegion", + }, + }, + ], + primaryKeys: ["id", "currency_code", "region_id"], + relationships: [ + { + serviceName: "currency", + primaryKey: "code", + foreignKey: "currency_code", + alias: "currency", + }, + { + serviceName: "region", + primaryKey: "id", + foreignKey: "region_id", + alias: "region", + }, + ], + extends: [ + { + serviceName: "currency", + fieldAlias: { + region: "region_link.region", + }, + relationship: { + serviceName: "currencyCurrencyRegionRegionLink", + primaryKey: "region_id", + foreignKey: "id", + alias: "region_link", + isList: false, + }, + }, + { + serviceName: "region", + fieldAlias: { + currency: "currency_link.currency", + }, + relationship: { + serviceName: "currencyCurrencyRegionRegionLink", + primaryKey: "currency_code", + foreignKey: "code", + alias: "currency_link", + isList: false, + }, + }, + ], + }) + }) + }) + }, +}) diff --git a/packages/core/medusa-test-utils/src/init-modules.ts b/packages/core/medusa-test-utils/src/init-modules.ts index 1de42a12c5..2ef4d55ade 100644 --- a/packages/core/medusa-test-utils/src/init-modules.ts +++ b/packages/core/medusa-test-utils/src/init-modules.ts @@ -5,7 +5,7 @@ import { } from "@medusajs/types" import { ContainerRegistrationKeys, - ModulesSdkUtils, + createPgConnection, promiseAll, } from "@medusajs/utils" @@ -41,7 +41,7 @@ export async function initModules({ let shouldDestroyConnectionAutomatically = !sharedPgConnection if (!sharedPgConnection) { - sharedPgConnection = ModulesSdkUtils.createPgConnection({ + sharedPgConnection = createPgConnection({ clientUrl: databaseConfig.clientUrl, schema: databaseConfig.schema, }) diff --git a/packages/core/modules-sdk/src/index.ts b/packages/core/modules-sdk/src/index.ts index fe00aabcc7..5df4a1e178 100644 --- a/packages/core/modules-sdk/src/index.ts +++ b/packages/core/modules-sdk/src/index.ts @@ -6,4 +6,3 @@ export * from "./medusa-module" export * from "./remote-link" export * from "./remote-query" export * from "./types" -export * from "./utils/define-link" diff --git a/packages/core/modules-sdk/src/utils/define-link.ts b/packages/core/modules-sdk/src/utils/define-link.ts deleted file mode 100644 index 5e1f6d796c..0000000000 --- a/packages/core/modules-sdk/src/utils/define-link.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { LinkModulesExtraFields, ModuleJoinerConfig } from "@medusajs/types" -import { - composeLinkName, - isObject, - isString, - toPascalCase, -} from "@medusajs/utils" -import { MedusaModule } from "../medusa-module" - -type ModuleLinkableKeyConfig = { - module: string - key: string - isList?: boolean - alias?: string - shortcuts?: { - [key: string]: string | { path: string; isList?: boolean } - } -} - -export function defineLink( - serviceAAndKey: string | ModuleLinkableKeyConfig, - serviceBAndKey: string | ModuleLinkableKeyConfig, - options?: { - pk?: { - [key: string]: string - } - database?: { - table: string - idPrefix?: string - extraColumns?: LinkModulesExtraFields - } - } -) { - const register = function ( - modules: ModuleJoinerConfig[] - ): ModuleJoinerConfig { - let serviceA: string - let serviceAKey: string - let serviceAIsList = false - let serviceAObj: Partial = {} - - let serviceB: string - let serviceBKey: string - let serviceBIsList = false - let serviceBObj: Partial = {} - - if (isString(serviceAAndKey)) { - let [mod, key] = (serviceAAndKey as string).split(".") - serviceA = mod - if (key.endsWith("[]")) { - serviceAIsList = true - key = key.slice(0, -2) - } - serviceAKey = key - } else if (isObject(serviceAAndKey)) { - const objA = serviceAAndKey as ModuleLinkableKeyConfig - serviceAObj = objA - - serviceA = objA.module - serviceAKey = objA.key - serviceAIsList = !!objA.isList - } else { - throw new Error("Invalid value for serviceA config") - } - - if (isString(serviceBAndKey)) { - let [mod, key] = (serviceBAndKey as string).split(".") - serviceB = mod - if (key.endsWith("[]")) { - serviceBIsList = true - key = key.slice(0, -2) - } - serviceBKey = key - } else if (isObject(serviceBAndKey)) { - const objB = serviceBAndKey as ModuleLinkableKeyConfig - serviceBObj = objB - - serviceB = objB.module - serviceBKey = objB.key - serviceBIsList = !!objB.isList - } else { - throw new Error("Invalid value for serviceB config") - } - - const serviceAInfo = modules.find((mod) => mod.serviceName === serviceA) - const serviceBInfo = modules.find((mod) => mod.serviceName === serviceB) - if (!serviceAInfo) { - throw new Error(`Service ${serviceA} was not found`) - } - if (!serviceBInfo) { - throw new Error(`Service ${serviceB} was not found`) - } - - const serviceAKeyInfo = serviceAInfo.linkableKeys?.[serviceAKey] - const serviceBKeyInfo = serviceBInfo.linkableKeys?.[serviceBKey] - if (!serviceAKeyInfo) { - throw new Error( - `Key ${serviceAKey} is not linkable on service ${serviceA}` - ) - } - if (!serviceBKeyInfo) { - throw new Error( - `Key ${serviceBKey} is not linkable on service ${serviceB}` - ) - } - - let serviceAAliases = serviceAInfo.alias ?? [] - if (!Array.isArray(serviceAAliases)) { - serviceAAliases = [serviceAAliases] - } - - let aliasAOptions = - serviceAObj.alias ?? - serviceAAliases.find((a) => { - return a.args?.entity == serviceAKeyInfo - })?.name - - let aliasA = "" - if (Array.isArray(aliasAOptions)) { - aliasA = aliasAOptions[0] - } - if (!aliasA) { - throw new Error( - `You need to provide an alias for ${serviceA}.${serviceAKey}` - ) - } - - let serviceBAliases = serviceBInfo.alias ?? [] - if (!Array.isArray(serviceBAliases)) { - serviceBAliases = [serviceBAliases] - } - - let aliasBOptions = - serviceBObj.alias ?? - serviceBAliases.find((a) => { - return a.args?.entity == serviceBKeyInfo - })?.name - - let aliasB = "" - if (Array.isArray(aliasBOptions)) { - aliasB = aliasBOptions[0] - } - if (!aliasB) { - throw new Error( - `You need to provide an alias for ${serviceB}.${serviceBKey}` - ) - } - - let serviceAPrimaryKey = options?.pk?.[serviceA] ?? serviceAInfo.primaryKeys - if (Array.isArray(serviceAPrimaryKey)) { - serviceAPrimaryKey = serviceAPrimaryKey[0] - } - - let serviceBPrimaryKey = options?.pk?.[serviceB] ?? serviceBInfo.primaryKeys - if (Array.isArray(serviceBPrimaryKey)) { - serviceBPrimaryKey = serviceBPrimaryKey[0] - } - - const serviceName = composeLinkName(serviceA, aliasA, serviceB, aliasB) - - const linkDefinition: ModuleJoinerConfig = { - serviceName, - isLink: true, - alias: [ - { - name: [aliasA + "_" + aliasB], - args: { - entity: toPascalCase( - ["Link", serviceA, aliasA, serviceB, aliasB].join("_") - ), - }, - }, - ], - primaryKeys: ["id", serviceAKey, serviceBKey], - relationships: [ - { - serviceName: serviceA, - primaryKey: serviceAPrimaryKey!, - foreignKey: serviceAKey, - alias: aliasA, - }, - { - serviceName: serviceB, - primaryKey: serviceBPrimaryKey!, - foreignKey: serviceBKey, - alias: aliasB, - }, - ], - extends: [ - { - serviceName: serviceA, - fieldAlias: { - [aliasB]: aliasB + "_link." + aliasB, //plural aliasA - }, - relationship: { - serviceName, - primaryKey: serviceAKey, - foreignKey: serviceBPrimaryKey!, - alias: aliasB + "_link", // plural alias - isList: serviceAIsList, - }, - }, - { - serviceName: serviceB, - fieldAlias: { - [aliasA]: aliasA + "_link." + aliasA, - }, - relationship: { - serviceName, - primaryKey: serviceBKey, - foreignKey: serviceAPrimaryKey!, - alias: aliasA + "_link", // plural alias - isList: serviceBIsList, - }, - }, - ], - } - - if (options?.database) { - const { table, idPrefix, extraColumns } = options.database - linkDefinition.databaseConfig = { - tableName: table, - idPrefix, - extraFields: extraColumns, - } - } - - return linkDefinition - } - - MedusaModule.setCustomLink(register) -} diff --git a/packages/core/modules-sdk/src/utils/index.ts b/packages/core/modules-sdk/src/utils/index.ts index ff694c9a3f..d1470bdcbc 100644 --- a/packages/core/modules-sdk/src/utils/index.ts +++ b/packages/core/modules-sdk/src/utils/index.ts @@ -1,3 +1,2 @@ export * from "./clean-graphql-schema" -export * from "./define-link" export * from "./graphql-schema-to-fields" diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index b5ebbcaf73..48c59473d4 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -19,11 +19,11 @@ export type IDmlEntityConfig = string | { name?: string; tableName: string } export type InferDmlEntityNameFromConfig = TConfig extends string - ? Capitalize> + ? CamelCase : TConfig extends { name: string } - ? Capitalize> + ? CamelCase : TConfig extends { tableName: string } - ? Capitalize> + ? CamelCase : never /** diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index 87123a94c3..f867c4b794 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -1,5 +1,5 @@ import { ConfigModule } from "@medusajs/types" -import { Modules } from "../modules-sdk" +import { Modules } from "../modules-sdk/definition" const DEFAULT_SECRET = "supersecret" const DEFAULT_ADMIN_URL = "http://localhost:9000" diff --git a/packages/core/utils/src/common/index.ts b/packages/core/utils/src/common/index.ts index eee51800e5..ddd15cfda7 100644 --- a/packages/core/utils/src/common/index.ts +++ b/packages/core/utils/src/common/index.ts @@ -12,7 +12,6 @@ export * from "./deduplicate" export * from "./deep-copy" export * from "./deep-equal-obj" export * from "./deep-flat-map" -export * from "./define-config" export * from "./errors" export * from "./file-system" export * from "./generate-entity-id" @@ -67,3 +66,4 @@ export * from "./trim-zeros" export * from "./upper-case-first" export * from "./validate-handle" export * from "./wrap-handler" +export * from "./define-config" diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index fd7310f851..70d19edffb 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -72,7 +72,7 @@ describe("Entity builder", () => { phones: model.array(), }) - expect(user.name).toEqual("User") + expect(user.name).toEqual("user") expect(user.parse().tableName).toEqual("user") const User = toMikroORMEntity(user) @@ -202,7 +202,7 @@ describe("Entity builder", () => { } ) - expect(user.name).toEqual("User") + expect(user.name).toEqual("user") expect(user.parse().tableName).toEqual("user_table") const User = toMikroORMEntity(user) @@ -324,7 +324,7 @@ describe("Entity builder", () => { } ) - expect(user.name).toEqual("UserRole") + expect(user.name).toEqual("userRole") expect(user.parse().tableName).toEqual("user_role") const User = toMikroORMEntity(user) @@ -3849,7 +3849,7 @@ describe("Entity builder", () => { }) expect(defineEmail).toThrow( - 'Cannot cascade delete "user" relationship(s) from "Email" entity. Child to parent cascades are not allowed' + 'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed' ) }) diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index 853c2c2425..6472055842 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -109,7 +109,7 @@ export class EntityBuilder { * * export default MyCustom */ - define( + define( nameOrConfig: TConfig, schema: Schema ): DmlEntity< diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 4ebb2db726..69395c913b 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -9,11 +9,11 @@ import { IsDmlEntity, QueryCondition, } from "@medusajs/types" -import { isObject, isString, toCamelCase, upperCaseFirst } from "../common" +import { isObject, isString, toCamelCase } from "../common" import { transformIndexWhere } from "./helpers/entity-builder/build-indexes" import { BelongsTo } from "./relations/belongs-to" -function extractNameAndTableName( +function extractNameAndTableName( nameOrConfig: Config ) { const result = { @@ -27,9 +27,8 @@ function extractNameAndTableName( if (isString(nameOrConfig)) { const [schema, ...rest] = nameOrConfig.split(".") const name = rest.length ? rest.join(".") : schema - result.name = upperCaseFirst( - toCamelCase(name) - ) as InferDmlEntityNameFromConfig + result.name = toCamelCase(name) as InferDmlEntityNameFromConfig + result.tableName = nameOrConfig } @@ -44,9 +43,7 @@ function extractNameAndTableName( const [schema, ...rest] = potentialName.split(".") const name = rest.length ? rest.join(".") : schema - result.name = upperCaseFirst( - toCamelCase(name) - ) as InferDmlEntityNameFromConfig + result.name = toCamelCase(name) as InferDmlEntityNameFromConfig result.tableName = nameOrConfig.tableName } @@ -59,7 +56,7 @@ function extractNameAndTableName( */ export class DmlEntity< Schema extends DMLSchema, - TConfig extends IDmlEntityConfig + const TConfig extends IDmlEntityConfig > implements IDmlEntity { [IsDmlEntity]: true = true diff --git a/packages/core/utils/src/index.ts b/packages/core/utils/src/index.ts index d11b798b67..66851b319d 100644 --- a/packages/core/utils/src/index.ts +++ b/packages/core/utils/src/index.ts @@ -1,7 +1,7 @@ +export * from "./common" export * from "./api-key" export * from "./auth" export * from "./bundles" -export * from "./common" export * from "./dal" export * from "./decorators" export * from "./defaults" diff --git a/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts b/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts index 1d24141521..8bb31bab13 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/joiner-config-builder.spec.ts @@ -25,6 +25,7 @@ import { ShippingOptionRule, ShippingProfile, } from "../__fixtures__/joiner-config/entities" +import { upperCaseFirst } from "../../common" describe("joiner-config-builder", () => { describe("defineJoiner | Mikro orm objects", () => { @@ -420,14 +421,15 @@ describe("joiner-config-builder", () => { }) 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, + user_id: upperCaseFirst(user.name), + car_number_plate: upperCaseFirst(car.name), }) }) }) @@ -453,12 +455,18 @@ describe("joiner-config-builder", () => { name: model.text(), }) - const car = model.define("car", { - id: model.id(), - number_plate: model.text().primaryKey(), - }) + const car = model.define( + { name: "car", tableName: "car" }, + { + id: model.id(), + number_plate: model.text().primaryKey(), + } + ) - const linkConfig = buildLinkConfigFromDmlObjects("myService", [user, car]) + const linkConfig = buildLinkConfigFromDmlObjects("myService", { + user, + car, + }) expectTypeOf(linkConfig).toMatchTypeOf<{ user: { diff --git a/packages/core/utils/src/modules-sdk/define-link.ts b/packages/core/utils/src/modules-sdk/define-link.ts new file mode 100644 index 0000000000..75fdfcf28c --- /dev/null +++ b/packages/core/utils/src/modules-sdk/define-link.ts @@ -0,0 +1,316 @@ +import { LinkModulesExtraFields, ModuleJoinerConfig } from "@medusajs/types" +import { isObject, pluralize, toPascalCase } from "../common" +import { composeLinkName } from "../link" + +type InputSource = { + serviceName: string + field: string + linkable: string + primaryKey: string +} + +type InputToJson = { + toJSON: () => InputSource +} + +type CombinedSource = Record & InputToJson + +type InputOptions = { + source: CombinedSource | InputSource + isList?: boolean +} + +type ExtraOptions = { + pk?: { + [key: string]: string + } + database?: { + table: string + idPrefix?: string + extraColumns?: LinkModulesExtraFields + } +} + +type DefineLinkInputSource = InputSource | InputOptions | CombinedSource + +type ModuleLinkableKeyConfig = { + module: string + key: string + isList?: boolean + primaryKey: string + alias: string + shortcuts?: { + [key: string]: string | { path: string; isList?: boolean } + } +} + +function isInputOptions(input: any): input is InputOptions { + return isObject(input) && "source" in input +} + +function isInputSource(input: any): input is InputSource { + return (isObject(input) && "serviceName" in input) || "toJSON" in input +} + +function isToJSON(input: any): input is InputToJson { + return isObject(input) && "toJSON" in input +} + +export function defineLink( + leftService: DefineLinkInputSource, + rightService: DefineLinkInputSource, + linkServiceOptions?: ExtraOptions +) { + let serviceAObj = {} as ModuleLinkableKeyConfig + let serviceBObj = {} as ModuleLinkableKeyConfig + + if (isInputSource(leftService)) { + const source = isToJSON(leftService) ? leftService.toJSON() : leftService + + serviceAObj = { + key: source.linkable, + alias: source.field, + primaryKey: source.primaryKey, + isList: false, + module: source.serviceName, + } + } else if (isInputOptions(leftService)) { + const source = isToJSON(leftService.source) + ? leftService.source.toJSON() + : leftService.source + + serviceAObj = { + key: source.linkable, + alias: source.field, + primaryKey: source.primaryKey, + isList: leftService.isList ?? false, + module: source.serviceName, + } + } else { + throw new Error("Invalid linkable passed for the first argument") + } + + if (isInputSource(rightService)) { + const source = isToJSON(rightService) ? rightService.toJSON() : rightService + + serviceBObj = { + key: source.linkable, + alias: source.field, + primaryKey: source.primaryKey, + isList: false, + module: source.serviceName, + } + } else if (isInputOptions(rightService)) { + const source = isToJSON(rightService.source) + ? rightService.source.toJSON() + : rightService.source + + serviceBObj = { + key: source.linkable, + alias: source.field, + primaryKey: source.primaryKey, + isList: rightService.isList ?? false, + module: source.serviceName, + } + } else { + throw new Error(`Invalid linkable passed for the second argument`) + } + + const output = { serviceName: "" } + + const register = function ( + modules: ModuleJoinerConfig[] + ): ModuleJoinerConfig { + const serviceAInfo = modules + .map((mod) => mod[serviceAObj.module]) + .filter(Boolean)[0] + const serviceBInfo = modules + .map((mod) => mod[serviceBObj.module]) + .filter(Boolean)[0] + if (!serviceAInfo) { + throw new Error(`Service ${serviceAObj.module} was not found`) + } + if (!serviceBInfo) { + throw new Error(`Service ${serviceBObj.module} was not found`) + } + + const serviceAKeyInfo = + serviceAInfo.__joinerConfig.linkableKeys?.[serviceAObj.key] + const serviceBKeyInfo = + serviceBInfo.__joinerConfig.linkableKeys?.[serviceBObj.key] + if (!serviceAKeyInfo) { + throw new Error( + `Key ${serviceAObj.key} is not linkable on service ${serviceAObj.module}` + ) + } + if (!serviceBKeyInfo) { + throw new Error( + `Key ${serviceBObj.key} is not linkable on service ${serviceBObj.module}` + ) + } + + let serviceAAliases = serviceAInfo.__joinerConfig.alias ?? [] + if (!Array.isArray(serviceAAliases)) { + serviceAAliases = [serviceAAliases] + } + + let aliasAOptions = + serviceAObj.alias ?? + serviceAAliases.find((a) => { + return a.args?.entity == serviceAKeyInfo + })?.name + + let aliasA = aliasAOptions + if (Array.isArray(aliasAOptions)) { + aliasA = aliasAOptions[0] + } + if (!aliasA) { + throw new Error( + `You need to provide an alias for ${serviceAObj.module}.${serviceAObj.key}` + ) + } + + let serviceBAliases = serviceBInfo.__joinerConfig.alias ?? [] + if (!Array.isArray(serviceBAliases)) { + serviceBAliases = [serviceBAliases] + } + + let aliasBOptions = + serviceBObj.alias ?? + serviceBAliases.find((a) => { + return a.args?.entity == serviceBKeyInfo + })?.name + + let aliasB = aliasBOptions + if (Array.isArray(aliasBOptions)) { + aliasB = aliasBOptions[0] + } + if (!aliasB) { + throw new Error( + `You need to provide an alias for ${serviceBObj.module}.${serviceBObj.key}` + ) + } + + const moduleAPrimaryKeys = serviceAInfo.__joinerConfig.primaryKeys + let serviceAPrimaryKey = + serviceAObj.primaryKey ?? + linkServiceOptions?.pk?.[serviceAObj.module] ?? + moduleAPrimaryKeys + if (Array.isArray(serviceAPrimaryKey)) { + serviceAPrimaryKey = serviceAPrimaryKey[0] + } + + const isModuleAPrimaryKeyValid = + moduleAPrimaryKeys.includes(serviceAPrimaryKey) + if (!isModuleAPrimaryKeyValid) { + throw new Error( + `Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}` + ) + } + + const moduleBPrimaryKeys = serviceBInfo.__joinerConfig.primaryKeys + let serviceBPrimaryKey = + serviceBObj.primaryKey ?? + linkServiceOptions?.pk?.[serviceBObj.module] ?? + moduleBPrimaryKeys + if (Array.isArray(serviceBPrimaryKey)) { + serviceBPrimaryKey = serviceBPrimaryKey[0] + } + + const isModuleBPrimaryKeyValid = + moduleBPrimaryKeys.includes(serviceBPrimaryKey) + if (!isModuleBPrimaryKeyValid) { + throw new Error( + `Primary key ${serviceBPrimaryKey} is not defined on service ${serviceBObj.module}` + ) + } + + output.serviceName = composeLinkName( + serviceAObj.module, + aliasA, + serviceBObj.module, + aliasB + ) + + const linkDefinition: ModuleJoinerConfig = { + serviceName: output.serviceName, + isLink: true, + alias: [ + { + name: [aliasA + "_" + aliasB], + args: { + entity: toPascalCase( + [ + "Link", + serviceAObj.module, + aliasA, + serviceBObj.module, + aliasB, + ].join("_") + ), + }, + }, + ], + primaryKeys: ["id", serviceAObj.key, serviceBObj.key], + relationships: [ + { + serviceName: serviceAObj.module, + primaryKey: serviceAPrimaryKey, + foreignKey: serviceAObj.key, + alias: aliasA, + }, + { + serviceName: serviceBObj.module, + primaryKey: serviceBPrimaryKey!, + foreignKey: serviceBObj.key, + alias: aliasB, + }, + ], + extends: [ + { + serviceName: serviceAObj.module, + fieldAlias: { + [serviceBObj.isList ? pluralize(aliasB) : aliasB]: + aliasB + "_link." + aliasB, //plural aliasA + }, + relationship: { + serviceName: output.serviceName, + primaryKey: serviceBObj.key, + foreignKey: serviceBPrimaryKey, + alias: aliasB + "_link", // plural alias + isList: serviceBObj.isList, + }, + }, + { + serviceName: serviceBObj.module, + fieldAlias: { + [serviceAObj.isList ? pluralize(aliasA) : aliasA]: + aliasA + "_link." + aliasA, + }, + relationship: { + serviceName: output.serviceName, + primaryKey: serviceAObj.key, + foreignKey: serviceAPrimaryKey, + alias: aliasA + "_link", // plural alias + isList: serviceAObj.isList, + }, + }, + ], + } + + if (linkServiceOptions?.database) { + const { table, idPrefix, extraColumns } = linkServiceOptions.database + linkDefinition.databaseConfig = { + tableName: table, + idPrefix, + extraFields: extraColumns, + } + } + + return linkDefinition + } + + global.MedusaModule.setCustomLink(register) + + return output +} diff --git a/packages/core/utils/src/modules-sdk/index.ts b/packages/core/utils/src/modules-sdk/index.ts index 7cfe583322..d8ff0499fb 100644 --- a/packages/core/utils/src/modules-sdk/index.ts +++ b/packages/core/utils/src/modules-sdk/index.ts @@ -14,3 +14,4 @@ export * from "./medusa-service" export * from "./migration-scripts" export * from "./mikro-orm-cli-config-builder" export * from "./module" +export * from "./define-link" diff --git a/packages/core/utils/src/modules-sdk/joiner-config-builder.ts b/packages/core/utils/src/modules-sdk/joiner-config-builder.ts index 591c8005cb..9762620783 100644 --- a/packages/core/utils/src/modules-sdk/joiner-config-builder.ts +++ b/packages/core/utils/src/modules-sdk/joiner-config-builder.ts @@ -1,4 +1,5 @@ import { + IDmlEntity, JoinerServiceConfigAlias, ModuleJoinerConfig, PropertyType, @@ -108,13 +109,14 @@ export function defineJoinerConfig( } if (!primaryKeys && modelDefinitions.size) { - const linkConfig = buildLinkConfigFromDmlObjects(serviceName, [ - ...modelDefinitions.values(), - ]) + const linkConfig = buildLinkConfigFromDmlObjects( + serviceName, + Object.fromEntries(modelDefinitions) + ) primaryKeys = deduplicate( Object.values(linkConfig).flatMap((entityLinkConfig) => { - return (Object.values(entityLinkConfig) as any[]) + return (Object.values(entityLinkConfig as any) as any[]) .filter((linkableConfig) => isObject(linkableConfig)) .map((linkableConfig) => { return linkableConfig.primaryKey @@ -152,7 +154,7 @@ export function defineJoinerConfig( `${pluralize(camelToSnakeCase(entity.name).toLowerCase())}`, ], args: { - entity: entity.name, + entity: upperCaseFirst(entity.name), methodSuffix: pluralize(upperCaseFirst(entity.name)), }, })), @@ -175,6 +177,8 @@ export function defineJoinerConfig( * test: model.text(), * }) * + * const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) + * * // output: * // { * // user_id: 'User', @@ -213,7 +217,7 @@ export function buildLinkableKeysFromDmlObjects< if (primaryKeys.length) { primaryKeys.forEach((primaryKey) => { - linkableKeys[primaryKey] = model.name + linkableKeys[primaryKey] = upperCaseFirst(model.name) }) } } @@ -250,7 +254,7 @@ export function buildLinkableKeysFromMikroOrmObjects( * test: model.text(), * }) * - * const links = buildLinkConfigFromDmlObjects('userService', [user, car]) + * const links = buildLinkConfigFromDmlObjects('userService', { user, car }) * * // output: * // { @@ -279,19 +283,17 @@ export function buildLinkableKeysFromMikroOrmObjects( */ export function buildLinkConfigFromDmlObjects< const ServiceName extends string, - const T extends DmlEntity[] ->( - serviceName: ServiceName, - models: T = [] as unknown as T -): InfersLinksConfig { + const T extends Record> +>(serviceName: ServiceName, models: T): InfersLinksConfig { const linkConfig = {} as InfersLinksConfig - for (const model of models) { + for (const model of Object.values(models) ?? []) { if (!DmlEntity.isDmlEntity(model)) { continue } const schema = model.schema + // @ts-ignore const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= { toJSON: function () { const linkables = Object.entries(this) @@ -322,7 +324,7 @@ export function buildLinkConfigFromDmlObjects< } } - return linkConfig as InfersLinksConfig & Record + return linkConfig as InfersLinksConfig } /** diff --git a/packages/core/utils/src/modules-sdk/medusa-service.ts b/packages/core/utils/src/modules-sdk/medusa-service.ts index 77bf317dba..6c87d8ad7b 100644 --- a/packages/core/utils/src/modules-sdk/medusa-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-service.ts @@ -353,13 +353,12 @@ export function MedusaService< class AbstractModuleService_ { [MedusaServiceSymbol] = true - static [MedusaServiceModelObjectsSymbol] = Object.values( - entities - ) as unknown as MedusaServiceReturnType< - EntitiesConfig extends { __empty: any } - ? ModelConfigurationsToConfigTemplate - : EntitiesConfig - >["$modelObjects"]; + static [MedusaServiceModelObjectsSymbol] = + entities as unknown as MedusaServiceReturnType< + EntitiesConfig extends { __empty: any } + ? ModelConfigurationsToConfigTemplate + : EntitiesConfig + >["$modelObjects"]; [MedusaServiceEntityNameToLinkableKeysMapSymbol]: MapToConfig diff --git a/packages/core/utils/src/modules-sdk/module.ts b/packages/core/utils/src/modules-sdk/module.ts index 444ce7c697..e7ef42ddfc 100644 --- a/packages/core/utils/src/modules-sdk/module.ts +++ b/packages/core/utils/src/modules-sdk/module.ts @@ -1,11 +1,10 @@ -import { Constructor, ModuleExports } from "@medusajs/types" +import { Constructor, IDmlEntity, 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 @@ -19,28 +18,32 @@ import { DmlEntity } from "../dml" export function Module< const ServiceName extends string, const Service extends Constructor, - const ModelObjects extends DmlEntity[] = Service extends { - $modelObjects: infer $DmlObjects + ModelObjects extends Record> = Service extends { + $modelObjects: any } - ? $DmlObjects - : [], - Links = keyof ModelObjects extends never + ? Service["$modelObjects"] + : {}, + Linkable = keyof ModelObjects extends never ? Record : InfersLinksConfig >( serviceName: ServiceName, { service, loaders }: ModuleExports ): ModuleExports & { - links: Links + linkable: Linkable } { service.prototype.__joinerConfig ??= defineJoinerConfig(serviceName) - const dmlObjects = service[MedusaServiceModelObjectsSymbol] + const dmlObjects = service[MedusaServiceModelObjectsSymbol] ?? {} + return { service, loaders, - links: (dmlObjects?.length - ? buildLinkConfigFromDmlObjects(dmlObjects) - : {}) as Links, + linkable: (Object.keys(dmlObjects)?.length + ? buildLinkConfigFromDmlObjects( + serviceName, + dmlObjects + ) + : {}) as Linkable, } } diff --git a/packages/core/utils/src/modules-sdk/types/links-config.ts b/packages/core/utils/src/modules-sdk/types/links-config.ts index c0391caa73..c2b42b4fa3 100644 --- a/packages/core/utils/src/modules-sdk/types/links-config.ts +++ b/packages/core/utils/src/modules-sdk/types/links-config.ts @@ -1,10 +1,11 @@ import { DMLSchema, + IDmlEntity, IDmlEntityConfig, InferDmlEntityNameFromConfig, + Prettify, SnakeCase, } from "@medusajs/types" -import { DmlEntity } from "../../dml" import { PrimaryKeyModifier } from "../../dml/properties/primary-key" /** @@ -54,22 +55,22 @@ type InferLinkableKeyName< string}` : never -type InferSchemaLinkableKeys = T extends DmlEntity< +type InferSchemaLinkableKeys = T extends IDmlEntity< infer Schema, infer Config > ? { [K in keyof Schema as Schema[K] extends PrimaryKeyModifier ? InferLinkableKeyName - : never]: InferDmlEntityNameFromConfig + : never]: Capitalize> } : {} -type InferSchemasLinkableKeys[]> = { +type InferSchemasLinkableKeys[]> = { [K in keyof T]: InferSchemaLinkableKeys } -type AggregateSchemasLinkableKeys[]> = { +type AggregateSchemasLinkableKeys[]> = { [K in keyof InferSchemasLinkableKeys]: InferSchemasLinkableKeys[K] } @@ -92,7 +93,7 @@ type AggregateSchemasLinkableKeys[]> = { * const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' } * */ -export type InferLinkableKeys[]> = +export type InferLinkableKeys[]> = UnionToIntersection>[0]> /** @@ -141,14 +142,12 @@ type InferPrimaryKeyNameOrNever< type InferSchemaLinksConfig< ServiceName extends string, T -> = T extends DmlEntity +> = T extends IDmlEntity ? { - [K in keyof Schema as Schema[K] extends PrimaryKeyModifier - ? InferPrimaryKeyNameOrNever - : never]: { + [K in keyof Schema as InferPrimaryKeyNameOrNever]: { serviceName: ServiceName - field: T extends DmlEntity - ? Uncapitalize> + field: T extends IDmlEntity + ? InferDmlEntityNameFromConfig : string linkable: InferLinkableKeyName primaryKey: K @@ -200,20 +199,22 @@ type InferSchemaLinksConfig< */ export type InfersLinksConfig< ServiceName extends string, - T extends DmlEntity[] -> = UnionToIntersection<{ - [K in keyof T as T[K] extends DmlEntity - ? Uncapitalize> - : never]: InferSchemaLinksConfig & { - toJSON: () => { - serviceName: ServiceName - field: T[K] extends DmlEntity - ? Uncapitalize> - : string - linkable: InferLastLinkable - primaryKey: InferLastPrimaryKey + T extends Record> +> = Prettify<{ + [K in keyof T as T[K] extends IDmlEntity + ? InferDmlEntityNameFromConfig + : never]: Prettify< + InferSchemaLinksConfig & { + toJSON: () => { + serviceName: ServiceName + field: T[K] extends IDmlEntity + ? InferDmlEntityNameFromConfig + : string + linkable: InferLastLinkable + primaryKey: InferLastPrimaryKey + } } - } + > }> /** diff --git a/packages/core/utils/src/modules-sdk/types/medusa-service.ts b/packages/core/utils/src/modules-sdk/types/medusa-service.ts index b65db0968c..be03c1c6b7 100644 --- a/packages/core/utils/src/modules-sdk/types/medusa-service.ts +++ b/packages/core/utils/src/modules-sdk/types/medusa-service.ts @@ -2,6 +2,7 @@ import { Constructor, Context, FindConfig, + IDmlEntity, Pluralize, RestoreReturn, SoftDeleteReturn, @@ -42,7 +43,7 @@ export type ModelConfigurationsToConfigTemplate = { dto: T[Key] extends Constructor ? InstanceType : any model: T[Key] extends { model: infer MODEL } ? MODEL - : T[Key] extends DmlEntity + : T[Key] extends IDmlEntity ? T[Key] : never /** @@ -242,20 +243,20 @@ export type AbstractModuleService< type InferModelFromConfig = { [K in keyof T as T[K] extends { model: any } ? K - : K extends DmlEntity + : K extends IDmlEntity ? K : never]: T[K] extends { model: infer MODEL } - ? MODEL extends DmlEntity + ? MODEL extends IDmlEntity ? MODEL : never - : T[K] extends DmlEntity + : T[K] extends IDmlEntity ? T[K] : never } export type MedusaServiceReturnType> = { new (...args: any[]): AbstractModuleService - $modelObjects: InferModelFromConfig[keyof InferModelFromConfig][] + $modelObjects: InferModelFromConfig } diff --git a/packages/modules/region/src/index.ts b/packages/modules/region/src/index.ts index b5eec03e31..9e24ccddc5 100644 --- a/packages/modules/region/src/index.ts +++ b/packages/modules/region/src/index.ts @@ -1,9 +1,8 @@ -import { ModuleExports } from "@medusajs/types" import { RegionModuleService } from "./services" import loadDefaults from "./loaders/defaults" +import { Module, Modules } from "@medusajs/utils" -const moduleDefinition: ModuleExports = { +export default Module(Modules.REGION, { service: RegionModuleService, loaders: [loadDefaults], -} -export default moduleDefinition +}) diff --git a/packages/modules/region/src/loaders/defaults.ts b/packages/modules/region/src/loaders/defaults.ts index 009065364e..0d024643e3 100644 --- a/packages/modules/region/src/loaders/defaults.ts +++ b/packages/modules/region/src/loaders/defaults.ts @@ -1,13 +1,13 @@ import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" import { ContainerRegistrationKeys, DefaultsUtils } from "@medusajs/utils" -import { RegionCountry } from "@models" +import { Country } from "@models" export default async ({ container }: LoaderOptions): Promise => { // TODO: Add default logger to the container when running tests const logger = container.resolve(ContainerRegistrationKeys.LOGGER) ?? console const countryService_: ModulesSdkTypes.IMedusaInternalService< - typeof RegionCountry + typeof Country > = container.resolve("countryService") try { diff --git a/packages/modules/region/src/models/country.ts b/packages/modules/region/src/models/country.ts index 6178307ee6..460e43d8d9 100644 --- a/packages/modules/region/src/models/country.ts +++ b/packages/modules/region/src/models/country.ts @@ -1,7 +1,7 @@ import { model } from "@medusajs/utils" import Region from "./region" -const Country = model +export default model .define( { name: "Country", tableName: "region_country" }, { @@ -24,5 +24,3 @@ const Country = model unique: true, }, ]) - -export default Country diff --git a/packages/modules/region/src/models/index.ts b/packages/modules/region/src/models/index.ts index 95d96d14bd..f4e91bedbc 100644 --- a/packages/modules/region/src/models/index.ts +++ b/packages/modules/region/src/models/index.ts @@ -1,3 +1,2 @@ -export { default as RegionCountry } from "./country" +export { default as Country } from "./country" export { default as Region } from "./region" - diff --git a/packages/modules/region/src/models/region.ts b/packages/modules/region/src/models/region.ts index 014c5b0a13..fa643107bb 100644 --- a/packages/modules/region/src/models/region.ts +++ b/packages/modules/region/src/models/region.ts @@ -1,7 +1,7 @@ import { model } from "@medusajs/utils" import RegionCountry from "./country" -const Region = model.define("region", { +export default model.define("region", { id: model.id({ prefix: "reg" }).primaryKey(), name: model.text().searchable(), currency_code: model.text().searchable(), @@ -9,5 +9,3 @@ const Region = model.define("region", { countries: model.hasMany(() => RegionCountry), metadata: model.json().nullable(), }) - -export default Region diff --git a/packages/modules/region/src/services/region-module.ts b/packages/modules/region/src/services/region-module.ts index 4f0dc1d791..e5e4e40412 100644 --- a/packages/modules/region/src/services/region-module.ts +++ b/packages/modules/region/src/services/region-module.ts @@ -26,7 +26,7 @@ import { promiseAll, removeUndefined, } from "@medusajs/utils" -import { Region, RegionCountry as Country } from "@models" +import { Country, Region } from "@models" import { UpdateRegionInput } from "@types" import { joinerConfig } from "../joiner-config" @@ -40,9 +40,11 @@ export default class RegionModuleService extends MedusaService<{ Region: { dto: RegionDTO + model: typeof Region } Country: { dto: RegionCountryDTO + model: typeof Country } }>({ Region, Country }) implements IRegionModuleService