Feat/mikro orm based linkable (#7944)

**What**
- Generate simple linkable for mikro orm based modules 
- fix module util
- fix joiner config builder
- fix define link relationship extension
- fix migrations not loading custom links
This commit is contained in:
Adrien de Peretti
2024-07-04 15:30:47 +02:00
committed by GitHub
parent d036130604
commit 7b84d854f0
10 changed files with 306 additions and 78 deletions

View File

@@ -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,
},

View File

@@ -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
))
}
}

View File

@@ -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,
})

View File

@@ -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,
},

View File

@@ -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<string, DmlEntity<any, any>>(
loadedModels!
.filter(
(model): model is DmlEntity<any, any> => !!DmlEntity.isDmlEntity(model)
)
.map((model) => [model.name, model])
)
const mikroOrmObjects = new Map(
loadedModels
.filter((model) => !DmlEntity.isDmlEntity(model))
const mikroOrmObjects = new Map<string, Function>(
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<any, any>
| { 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<string, IDmlEntity<any, any>>
>(serviceName: ServiceName, models: T): InfersLinksConfig<ServiceName, T> {
@@ -327,6 +365,40 @@ export function buildLinkConfigFromDmlObjects<
return linkConfig as InfersLinksConfig<ServiceName, T>
}
/**
* @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<string, string>
>(serviceName: ServiceName, linkableKeys: T): Record<string, any> {
const linkConfig = {} as Record<string, any>
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<string, any>
}
/**
* Reversed map from linkableKeys to entity name to linkable keys
* @param linkableKeys

View File

@@ -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<Service> & {
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>(
serviceName,
modelObjects
) as Linkable
} else {
linkable = buildLinkConfigFromLinkableKeys(
serviceName,
defaultJoinerConfig.linkableKeys
) as Linkable
}
}
return {
service,
loaders,
linkable: (Object.keys(dmlObjects)?.length
? buildLinkConfigFromDmlObjects<ServiceName, ModelObjects>(
serviceName,
dmlObjects
)
: {}) as Linkable,
linkable,
}
}

View File

@@ -174,7 +174,7 @@ type InferSchemaLinksConfig<
* test: model.text(),
* })
*
* const linkConfig = buildLinkConfigFromDmlObjects([user, car])
* const linkConfig = buildLinkConfigFromModelObjects([user, car])
* // {
* // user: {
* // id: {

View File

@@ -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") {

View File

@@ -48,8 +48,8 @@ export function mergeDefaultModules(
const orignalDef = value?.definition
if (isObject(orignalDef)) {
value.definition = {
...orignalDef,
...def,
...orignalDef,
}
}
}

View File

@@ -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 || {}
)
}