diff --git a/integration-tests/modules/__tests__/link-modules/define-link.spec.ts b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts index a596010123..b1ff00e21d 100644 --- a/integration-tests/modules/__tests__/link-modules/define-link.spec.ts +++ b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts @@ -25,7 +25,9 @@ medusaIntegrationTestRunner({ const linkDefinition = MedusaModule.getCustomLinks() .map((linkDefinition: any) => { - const definition = linkDefinition(MedusaModule.getLoadedModules()) + const definition = linkDefinition( + MedusaModule.getAllJoinerConfigs() + ) return definition.serviceName === link.serviceName && definition }) .filter(Boolean)[0] @@ -49,12 +51,18 @@ medusaIntegrationTestRunner({ primaryKey: "code", foreignKey: "currency_code", alias: "currency", + args: { + methodSuffix: "Currencies", + }, }, { serviceName: "region", primaryKey: "id", foreignKey: "region_id", alias: "region", + args: { + methodSuffix: "Regions", + }, }, ], extends: [ @@ -65,8 +73,8 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "currencyCurrencyRegionRegionLink", - primaryKey: "region_id", - foreignKey: "id", + primaryKey: "currency_code", + foreignKey: "code", alias: "region_link", isList: false, }, @@ -78,8 +86,8 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "currencyCurrencyRegionRegionLink", - primaryKey: "currency_code", - foreignKey: "code", + primaryKey: "region_id", + foreignKey: "id", alias: "currency_link", isList: false, }, diff --git a/packages/core/modules-sdk/src/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index c0d5dc4214..449ba127c7 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -18,12 +18,12 @@ import type { } from "@medusajs/types" import { ContainerRegistrationKeys, - ModuleRegistrationName, - Modules, - ModulesSdkUtils, createMedusaContainer, isObject, isString, + ModuleRegistrationName, + Modules, + ModulesSdkUtils, promiseAll, } from "@medusajs/utils" import { asValue } from "awilix" @@ -435,18 +435,30 @@ async function MedusaApp_({ ...(sharedResourcesConfig?.database ?? {}), } + const customLinks = MedusaModule.getCustomLinks().map((link) => { + return typeof link === "function" + ? link(MedusaModule.getAllJoinerConfigs()) + : link + }) + if (revert) { revertLinkModuleMigration && - (await revertLinkModuleMigration({ - options: linkModuleOpt, - injectedDependencies, - })) + (await revertLinkModuleMigration( + { + options: linkModuleOpt, + injectedDependencies, + }, + customLinks + )) } else { linkModuleMigration && - (await linkModuleMigration({ - options: linkModuleOpt, - injectedDependencies, - })) + (await linkModuleMigration( + { + options: linkModuleOpt, + injectedDependencies, + }, + customLinks + )) } } 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 8bb31bab13..5dfa02996c 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 @@ -1,7 +1,8 @@ import { buildLinkableKeysFromDmlObjects, buildLinkableKeysFromMikroOrmObjects, - buildLinkConfigFromDmlObjects, + buildLinkConfigFromLinkableKeys, + buildLinkConfigFromModelObjects, defineJoinerConfig, } from "../joiner-config-builder" import { Modules } from "../definition" @@ -448,7 +449,96 @@ describe("joiner-config-builder", () => { }) }) - describe("buildLinkConfigFromDmlObjects", () => { + describe("buildLinkConfigFromLinkableKeys", () => { + it("should return a link config object based on the linkable keys", () => { + class User {} + class Car {} + + const linkableKeys = buildLinkableKeysFromMikroOrmObjects([Car, User]) + + const linkConfig = buildLinkConfigFromLinkableKeys( + "myService", + linkableKeys + ) + + expect(linkConfig).toEqual({ + car: { + id: { + field: "car", + linkable: "car_id", + primaryKey: "id", + serviceName: "myService", + }, + toJSON: expect.any(Function), + }, + user: { + id: { + field: "user", + linkable: "user_id", + primaryKey: "id", + serviceName: "myService", + }, + toJSON: expect.any(Function), + }, + }) + + expect(linkConfig.car.toJSON()).toEqual({ + field: "car", + linkable: "car_id", + primaryKey: "id", + serviceName: "myService", + }) + expect(linkConfig.user.toJSON()).toEqual({ + field: "user", + linkable: "user_id", + primaryKey: "id", + serviceName: "myService", + }) + }) + + it("should return a link config object based on the custom linkable keys", () => { + const linkConfig = buildLinkConfigFromLinkableKeys("myService", { + user_id: "User", + currency_code: "currency", + }) + + expect(linkConfig).toEqual({ + user: { + id: { + field: "user", + linkable: "user_id", + primaryKey: "id", + serviceName: "myService", + }, + toJSON: expect.any(Function), + }, + currency: { + code: { + field: "currency", + linkable: "currency_code", + primaryKey: "code", + serviceName: "myService", + }, + toJSON: expect.any(Function), + }, + }) + + expect(linkConfig.user.toJSON()).toEqual({ + field: "user", + linkable: "user_id", + primaryKey: "id", + serviceName: "myService", + }) + expect(linkConfig.currency.toJSON()).toEqual({ + field: "currency", + linkable: "currency_code", + primaryKey: "code", + serviceName: "myService", + }) + }) + }) + + describe("buildLinkConfigFromModelObjects", () => { it("should return a link config object based on the DML's primary keys", () => { const user = model.define("user", { id: model.id().primaryKey(), @@ -463,7 +553,7 @@ describe("joiner-config-builder", () => { } ) - const linkConfig = buildLinkConfigFromDmlObjects("myService", { + const linkConfig = buildLinkConfigFromModelObjects("myService", { user, car, }) diff --git a/packages/core/utils/src/modules-sdk/define-link.ts b/packages/core/utils/src/modules-sdk/define-link.ts index 75fdfcf28c..8c06729b4a 100644 --- a/packages/core/utils/src/modules-sdk/define-link.ts +++ b/packages/core/utils/src/modules-sdk/define-link.ts @@ -121,12 +121,13 @@ export function defineLink( 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] + const serviceAInfo = modules.find( + (mod) => mod.serviceName === serviceAObj.module + )! + const serviceBInfo = modules.find( + (mod) => mod.serviceName === serviceBObj.module + )! + if (!serviceAInfo) { throw new Error(`Service ${serviceAObj.module} was not found`) } @@ -134,11 +135,9 @@ export function defineLink( 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) { + const serviceAKeyEntity = serviceAInfo.linkableKeys?.[serviceAObj.key] + const serviceBKeyInfo = serviceBInfo.linkableKeys?.[serviceBObj.key] + if (!serviceAKeyEntity) { throw new Error( `Key ${serviceAObj.key} is not linkable on service ${serviceAObj.module}` ) @@ -149,7 +148,7 @@ export function defineLink( ) } - let serviceAAliases = serviceAInfo.__joinerConfig.alias ?? [] + let serviceAAliases = serviceAInfo.alias ?? [] if (!Array.isArray(serviceAAliases)) { serviceAAliases = [serviceAAliases] } @@ -157,20 +156,27 @@ export function defineLink( let aliasAOptions = serviceAObj.alias ?? serviceAAliases.find((a) => { - return a.args?.entity == serviceAKeyInfo + return a.args?.entity == serviceAKeyEntity })?.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 ?? [] + const serviceAMethodSuffix = serviceAAliases.find((serviceAlias) => { + return Array.isArray(serviceAlias.name) + ? serviceAlias.name.includes(aliasA) + : serviceAlias.name === aliasA + })?.args?.methodSuffix + + let serviceBAliases = serviceBInfo.alias ?? [] if (!Array.isArray(serviceBAliases)) { serviceBAliases = [serviceBAliases] } @@ -185,13 +191,20 @@ export function defineLink( 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 + const serviceBMethodSuffix = serviceBAliases.find((serviceAlias) => { + return Array.isArray(serviceAlias.name) + ? serviceAlias.name.includes(aliasB) + : serviceAlias.name === aliasB + })?.args?.methodSuffix + + const moduleAPrimaryKeys = serviceAInfo.primaryKeys ?? [] let serviceAPrimaryKey = serviceAObj.primaryKey ?? linkServiceOptions?.pk?.[serviceAObj.module] ?? @@ -208,7 +221,7 @@ export function defineLink( ) } - const moduleBPrimaryKeys = serviceBInfo.__joinerConfig.primaryKeys + const moduleBPrimaryKeys = serviceBInfo.primaryKeys ?? [] let serviceBPrimaryKey = serviceBObj.primaryKey ?? linkServiceOptions?.pk?.[serviceBObj.module] ?? @@ -258,12 +271,18 @@ export function defineLink( primaryKey: serviceAPrimaryKey, foreignKey: serviceAObj.key, alias: aliasA, + args: { + methodSuffix: serviceAMethodSuffix, + }, }, { serviceName: serviceBObj.module, primaryKey: serviceBPrimaryKey!, foreignKey: serviceBObj.key, alias: aliasB, + args: { + methodSuffix: serviceBMethodSuffix, + }, }, ], extends: [ @@ -275,8 +294,8 @@ export function defineLink( }, relationship: { serviceName: output.serviceName, - primaryKey: serviceBObj.key, - foreignKey: serviceBPrimaryKey, + primaryKey: serviceAObj.key, + foreignKey: serviceAPrimaryKey, alias: aliasB + "_link", // plural alias isList: serviceBObj.isList, }, @@ -289,8 +308,8 @@ export function defineLink( }, relationship: { serviceName: output.serviceName, - primaryKey: serviceAObj.key, - foreignKey: serviceAPrimaryKey, + primaryKey: serviceBObj.key, + foreignKey: serviceBPrimaryKey, alias: aliasA + "_link", // plural alias isList: serviceAObj.isList, }, 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 9762620783..87075d0a66 100644 --- a/packages/core/utils/src/modules-sdk/joiner-config-builder.ts +++ b/packages/core/utils/src/modules-sdk/joiner-config-builder.ts @@ -4,6 +4,7 @@ import { ModuleJoinerConfig, PropertyType, } from "@medusajs/types" +import * as path from "path" import { dirname, join } from "path" import { camelToSnakeCase, @@ -13,6 +14,7 @@ import { lowerCaseFirst, MapToConfig, pluralize, + toCamelCase, upperCaseFirst, } from "../common" import { loadModels } from "./loaders/load-models" @@ -20,6 +22,7 @@ import { DmlEntity } from "../dml" import { BaseRelationship } from "../dml/relations/base" import { PrimaryKeyModifier } from "../dml/properties/primary-key" import { InferLinkableKeys, InfersLinksConfig } from "./types/links-config" +import { accessSync } 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 @@ -60,39 +63,74 @@ export function defineJoinerConfig( "serviceName" | "primaryKeys" | "linkableKeys" | "alias" > > { - const fullPath = getCallerFilePath() - const srcDir = fullPath.includes("dist") ? "dist" : "src" - const splitPath = fullPath.split(srcDir) + let loadedModels = models - let basePath = splitPath[0] + srcDir + if (!loadedModels) { + loadedModels = [] - const isMedusaProject = fullPath.includes(`${srcDir}/modules/`) - if (isMedusaProject) { - basePath = dirname(fullPath) + let index = 1 + const maxSearchIndex = 6 + + while (true) { + ++index + const fullPath = getCallerFilePath(index) + if (!fullPath) { + break + } + + const srcDir = fullPath.includes("dist") ? "dist" : "src" + const splitPath = fullPath.split(srcDir) + + let basePath = splitPath[0] + srcDir + + const isMedusaProject = fullPath.includes(`${srcDir}/modules/`) + if (isMedusaProject) { + basePath = dirname(fullPath) + } + + basePath = join(basePath, "models") + let doesModelsDirExist = false + try { + accessSync(path.resolve(basePath)) + doesModelsDirExist = true + } catch (e) {} + + if (!doesModelsDirExist) { + continue + } + + loadedModels = loadModels(basePath) + + if (index === maxSearchIndex || loadedModels.length) { + break + } + } } - basePath = join(basePath, "models") - - let loadedModels = models ?? loadModels(basePath) - const modelDefinitions = new Map( - loadedModels - .filter((model) => !!DmlEntity.isDmlEntity(model)) + const modelDefinitions = new Map>( + loadedModels! + .filter( + (model): model is DmlEntity => !!DmlEntity.isDmlEntity(model) + ) .map((model) => [model.name, model]) ) - const mikroOrmObjects = new Map( - loadedModels - .filter((model) => !DmlEntity.isDmlEntity(model)) + const mikroOrmObjects = new Map( + loadedModels! + .filter((model): model is Function => !DmlEntity.isDmlEntity(model)) .map((model) => [model.name, model]) ) // We prioritize DML if there is any equivalent Mikro orm entities found - loadedModels = [...modelDefinitions.values()] + const deduplicatedLoadedModels = [...modelDefinitions.values()] as ( + | DmlEntity + | { name: string } + )[] mikroOrmObjects.forEach((model) => { if (modelDefinitions.has(model.name)) { return } - loadedModels.push(model) + deduplicatedLoadedModels.push(model) }) if (!linkableKeys) { @@ -109,7 +147,7 @@ export function defineJoinerConfig( } if (!primaryKeys && modelDefinitions.size) { - const linkConfig = buildLinkConfigFromDmlObjects( + const linkConfig = buildLinkConfigFromModelObjects( serviceName, Object.fromEntries(modelDefinitions) ) @@ -142,7 +180,7 @@ export function defineJoinerConfig( pluralize(upperCaseFirst(alias.args.entity)), }, })), - ...loadedModels + ...deduplicatedLoadedModels .filter((model) => { return ( !alias || !alias.some((alias) => alias.args?.entity === model.name) @@ -254,7 +292,7 @@ export function buildLinkableKeysFromMikroOrmObjects( * test: model.text(), * }) * - * const links = buildLinkConfigFromDmlObjects('userService', { user, car }) + * const links = buildLinkConfigFromModelObjects('userService', { user, car }) * * // output: * // { @@ -281,7 +319,7 @@ export function buildLinkableKeysFromMikroOrmObjects( * @param serviceName * @param models */ -export function buildLinkConfigFromDmlObjects< +export function buildLinkConfigFromModelObjects< const ServiceName extends string, const T extends Record> >(serviceName: ServiceName, models: T): InfersLinksConfig { @@ -327,6 +365,40 @@ export function buildLinkConfigFromDmlObjects< return linkConfig as InfersLinksConfig } +/** + * @deprecated temporary supports for mikro orm entities to get the linkable available from the module export while waiting for the migration to DML + * + * @param serviceName + * @param linkableKeys + */ +export function buildLinkConfigFromLinkableKeys< + const ServiceName extends string, + const T extends Record +>(serviceName: ServiceName, linkableKeys: T): Record { + const linkConfig = {} as Record + + for (const [linkable, modelName] of Object.entries(linkableKeys)) { + const kebabCasedModelName = camelToSnakeCase(toCamelCase(modelName)) + const inferredReferenceProperty = linkable.replace( + `${kebabCasedModelName}_`, + "" + ) + + const config = { + linkable: linkable, + primaryKey: inferredReferenceProperty, + serviceName, + field: lowerCaseFirst(modelName), + } + linkConfig[lowerCaseFirst(modelName)] = { + [inferredReferenceProperty]: config, + toJSON: () => config, + } + } + + return linkConfig as Record +} + /** * Reversed map from linkableKeys to entity name to linkable keys * @param linkableKeys diff --git a/packages/core/utils/src/modules-sdk/module.ts b/packages/core/utils/src/modules-sdk/module.ts index e7ef42ddfc..9ad7e1f362 100644 --- a/packages/core/utils/src/modules-sdk/module.ts +++ b/packages/core/utils/src/modules-sdk/module.ts @@ -1,19 +1,20 @@ import { Constructor, IDmlEntity, ModuleExports } from "@medusajs/types" import { MedusaServiceModelObjectsSymbol } from "./medusa-service" import { - buildLinkConfigFromDmlObjects, + buildLinkConfigFromLinkableKeys, + buildLinkConfigFromModelObjects, 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 + * Wrapper to build the module export and auto generate the joiner config if not already provided in the module service, as well as + * return a linkable object based on the models * * @param serviceName * @param service * @param loaders - * @constructor */ export function Module< const ServiceName extends string, @@ -32,18 +33,34 @@ export function Module< ): ModuleExports & { linkable: Linkable } { - service.prototype.__joinerConfig ??= defineJoinerConfig(serviceName) + const defaultJoinerConfig = defineJoinerConfig(serviceName) + service.prototype.__joinerConfig ??= () => defaultJoinerConfig - const dmlObjects = service[MedusaServiceModelObjectsSymbol] ?? {} + const modelObjects = service[MedusaServiceModelObjectsSymbol] ?? {} + + let linkable = {} as Linkable + + if (Object.keys(modelObjects)?.length) { + const dmlObjects = Object.entries(modelObjects).filter(([, model]) => + DmlEntity.isDmlEntity(model) + ) + + if (dmlObjects.length) { + linkable = buildLinkConfigFromModelObjects( + serviceName, + modelObjects + ) as Linkable + } else { + linkable = buildLinkConfigFromLinkableKeys( + serviceName, + defaultJoinerConfig.linkableKeys + ) as Linkable + } + } return { service, loaders, - linkable: (Object.keys(dmlObjects)?.length - ? buildLinkConfigFromDmlObjects( - serviceName, - dmlObjects - ) - : {}) as Linkable, + 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 c2b42b4fa3..9aa862a817 100644 --- a/packages/core/utils/src/modules-sdk/types/links-config.ts +++ b/packages/core/utils/src/modules-sdk/types/links-config.ts @@ -174,7 +174,7 @@ type InferSchemaLinksConfig< * test: model.text(), * }) * - * const linkConfig = buildLinkConfigFromDmlObjects([user, car]) + * const linkConfig = buildLinkConfigFromModelObjects([user, car]) * // { * // user: { * // id: { diff --git a/packages/medusa/src/commands/migrate.ts b/packages/medusa/src/commands/migrate.ts index 0b67e8e14f..aa5752fa5b 100644 --- a/packages/medusa/src/commands/migrate.ts +++ b/packages/medusa/src/commands/migrate.ts @@ -2,6 +2,8 @@ import Logger from "../loaders/logger" import { migrateMedusaApp, revertMedusaApp } from "../loaders/medusa-app" import { initializeContainer } from "../loaders" import { ContainerRegistrationKeys } from "@medusajs/utils" +import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins" +import { resolvePluginsLinks } from "../loaders/helpers/resolve-plugins-links" const main = async function ({ directory }) { const args = process.argv @@ -10,17 +12,25 @@ const main = async function ({ directory }) { args.shift() const container = await initializeContainer(directory) + const configModule = container.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) + const plugins = getResolvedPlugins(directory, configModule, true) || [] + const pluginLinks = await resolvePluginsLinks(plugins, container) + if (args[0] === "run") { - await migrateMedusaApp({ configModule, container }) + await migrateMedusaApp({ + configModule, + linkModules: pluginLinks, + container, + }) Logger.info("Migrations completed.") process.exit() } else if (args[0] === "revert") { - await revertMedusaApp({ configModule, container }) + await revertMedusaApp({ configModule, linkModules: pluginLinks, container }) Logger.info("Migrations reverted.") } else if (args[0] === "show") { diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts index ca56a33851..0bb78812ed 100644 --- a/packages/medusa/src/loaders/medusa-app.ts +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -48,8 +48,8 @@ export function mergeDefaultModules( const orignalDef = value?.definition if (isObject(orignalDef)) { value.definition = { - ...orignalDef, ...def, + ...orignalDef, } } } diff --git a/packages/modules/link-modules/src/services/dynamic-service-class.ts b/packages/modules/link-modules/src/services/dynamic-service-class.ts index 27c867c8c4..a53f6100ac 100644 --- a/packages/modules/link-modules/src/services/dynamic-service-class.ts +++ b/packages/modules/link-modules/src/services/dynamic-service-class.ts @@ -14,7 +14,7 @@ export function getModuleService( // database config if any fields are provided. if (!isDefined(joinerConfig_.extraDataFields)) { joinerConfig_.extraDataFields = Object.keys( - databaseConfig.extraFields || {} + databaseConfig?.extraFields || {} ) }