feat: define link util (#7931)

* feat: define link util

* handle pluralized fieldAlias for isList

* serviceName ar reference

* finalize

* todo

* WIP

* finalize

* fix tests

* update typings

* fix Module

* linkable

* update errors
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-07-04 10:37:30 +02:00
1 parent 9b1688944e
commit 41c4307fc7
26 files changed
+511 -329

No files matched your search

@@ -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,
},
},
],
})
})
})
},
})
@@ -5,7 +5,7 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
ModulesSdkUtils, createPgConnection,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
@@ -41,7 +41,7 @@ export async function initModules({
let shouldDestroyConnectionAutomatically = !sharedPgConnection let shouldDestroyConnectionAutomatically = !sharedPgConnection
if (!sharedPgConnection) { if (!sharedPgConnection) {
sharedPgConnection = ModulesSdkUtils.createPgConnection({ sharedPgConnection = createPgConnection({
clientUrl: databaseConfig.clientUrl, clientUrl: databaseConfig.clientUrl,
schema: databaseConfig.schema, schema: databaseConfig.schema,
}) })
-1
View File
@@ -6,4 +6,3 @@ export * from "./medusa-module"
export * from "./remote-link" export * from "./remote-link"
export * from "./remote-query" export * from "./remote-query"
export * from "./types" export * from "./types"
export * from "./utils/define-link"
@@ -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<ModuleLinkableKeyConfig> = {}
let serviceB: string
let serviceBKey: string
let serviceBIsList = false
let serviceBObj: Partial<ModuleLinkableKeyConfig> = {}
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)
}
@@ -1,3 +1,2 @@
export * from "./clean-graphql-schema" export * from "./clean-graphql-schema"
export * from "./define-link"
export * from "./graphql-schema-to-fields" export * from "./graphql-schema-to-fields"
+3 -3
View File
@@ -19,11 +19,11 @@ export type IDmlEntityConfig = string | { name?: string; tableName: string }
export type InferDmlEntityNameFromConfig<TConfig extends IDmlEntityConfig> = export type InferDmlEntityNameFromConfig<TConfig extends IDmlEntityConfig> =
TConfig extends string TConfig extends string
? Capitalize<CamelCase<TConfig>> ? CamelCase<TConfig>
: TConfig extends { name: string } : TConfig extends { name: string }
? Capitalize<CamelCase<TConfig["name"] & string>> ? CamelCase<TConfig["name"]>
: TConfig extends { tableName: string } : TConfig extends { tableName: string }
? Capitalize<CamelCase<TConfig["tableName"] & string>> ? CamelCase<TConfig["tableName"]>
: never : never
/** /**
@@ -1,5 +1,5 @@
import { ConfigModule } from "@medusajs/types" import { ConfigModule } from "@medusajs/types"
import { Modules } from "../modules-sdk" import { Modules } from "../modules-sdk/definition"
const DEFAULT_SECRET = "supersecret" const DEFAULT_SECRET = "supersecret"
const DEFAULT_ADMIN_URL = "http://localhost:9000" const DEFAULT_ADMIN_URL = "http://localhost:9000"
+1 -1
View File
@@ -12,7 +12,6 @@ export * from "./deduplicate"
export * from "./deep-copy" export * from "./deep-copy"
export * from "./deep-equal-obj" export * from "./deep-equal-obj"
export * from "./deep-flat-map" export * from "./deep-flat-map"
export * from "./define-config"
export * from "./errors" export * from "./errors"
export * from "./file-system" export * from "./file-system"
export * from "./generate-entity-id" export * from "./generate-entity-id"
@@ -67,3 +66,4 @@ export * from "./trim-zeros"
export * from "./upper-case-first" export * from "./upper-case-first"
export * from "./validate-handle" export * from "./validate-handle"
export * from "./wrap-handler" export * from "./wrap-handler"
export * from "./define-config"
@@ -72,7 +72,7 @@ describe("Entity builder", () => {
phones: model.array(), phones: model.array(),
}) })
expect(user.name).toEqual("User") expect(user.name).toEqual("user")
expect(user.parse().tableName).toEqual("user") expect(user.parse().tableName).toEqual("user")
const User = toMikroORMEntity(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") expect(user.parse().tableName).toEqual("user_table")
const User = toMikroORMEntity(user) 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") expect(user.parse().tableName).toEqual("user_role")
const User = toMikroORMEntity(user) const User = toMikroORMEntity(user)
@@ -3849,7 +3849,7 @@ describe("Entity builder", () => {
}) })
expect(defineEmail).toThrow( 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'
) )
}) })
@@ -109,7 +109,7 @@ export class EntityBuilder {
* *
* export default MyCustom * export default MyCustom
*/ */
define<Schema extends DMLSchema, TConfig extends IDmlEntityConfig>( define<Schema extends DMLSchema, const TConfig extends IDmlEntityConfig>(
nameOrConfig: TConfig, nameOrConfig: TConfig,
schema: Schema schema: Schema
): DmlEntity< ): DmlEntity<
+6 -9
View File
@@ -9,11 +9,11 @@ import {
IsDmlEntity, IsDmlEntity,
QueryCondition, QueryCondition,
} from "@medusajs/types" } 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 { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
import { BelongsTo } from "./relations/belongs-to" import { BelongsTo } from "./relations/belongs-to"
function extractNameAndTableName<Config extends IDmlEntityConfig>( function extractNameAndTableName<const Config extends IDmlEntityConfig>(
nameOrConfig: Config nameOrConfig: Config
) { ) {
const result = { const result = {
@@ -27,9 +27,8 @@ function extractNameAndTableName<Config extends IDmlEntityConfig>(
if (isString(nameOrConfig)) { if (isString(nameOrConfig)) {
const [schema, ...rest] = nameOrConfig.split(".") const [schema, ...rest] = nameOrConfig.split(".")
const name = rest.length ? rest.join(".") : schema const name = rest.length ? rest.join(".") : schema
result.name = upperCaseFirst( result.name = toCamelCase(name) as InferDmlEntityNameFromConfig<Config>
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig result.tableName = nameOrConfig
} }
@@ -44,9 +43,7 @@ function extractNameAndTableName<Config extends IDmlEntityConfig>(
const [schema, ...rest] = potentialName.split(".") const [schema, ...rest] = potentialName.split(".")
const name = rest.length ? rest.join(".") : schema const name = rest.length ? rest.join(".") : schema
result.name = upperCaseFirst( result.name = toCamelCase(name) as InferDmlEntityNameFromConfig<Config>
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig.tableName result.tableName = nameOrConfig.tableName
} }
@@ -59,7 +56,7 @@ function extractNameAndTableName<Config extends IDmlEntityConfig>(
*/ */
export class DmlEntity< export class DmlEntity<
Schema extends DMLSchema, Schema extends DMLSchema,
TConfig extends IDmlEntityConfig const TConfig extends IDmlEntityConfig
> implements IDmlEntity<Schema, TConfig> > implements IDmlEntity<Schema, TConfig>
{ {
[IsDmlEntity]: true = true [IsDmlEntity]: true = true
+1 -1
View File
@@ -1,7 +1,7 @@
export * from "./common"
export * from "./api-key" export * from "./api-key"
export * from "./auth" export * from "./auth"
export * from "./bundles" export * from "./bundles"
export * from "./common"
export * from "./dal" export * from "./dal"
export * from "./decorators" export * from "./decorators"
export * from "./defaults" export * from "./defaults"
@@ -25,6 +25,7 @@ import {
ShippingOptionRule, ShippingOptionRule,
ShippingProfile, ShippingProfile,
} from "../__fixtures__/joiner-config/entities" } from "../__fixtures__/joiner-config/entities"
import { upperCaseFirst } from "../../common"
describe("joiner-config-builder", () => { describe("joiner-config-builder", () => {
describe("defineJoiner | Mikro orm objects", () => { describe("defineJoiner | Mikro orm objects", () => {
@@ -420,14 +421,15 @@ describe("joiner-config-builder", () => {
}) })
const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) const linkableKeys = buildLinkableKeysFromDmlObjects([user, car])
expectTypeOf(linkableKeys).toMatchTypeOf<{ expectTypeOf(linkableKeys).toMatchTypeOf<{
user_id: "User" user_id: "User"
car_number_plate: "Car" car_number_plate: "Car"
}>() }>()
expect(linkableKeys).toEqual({ expect(linkableKeys).toEqual({
user_id: user.name, user_id: upperCaseFirst(user.name),
car_number_plate: car.name, car_number_plate: upperCaseFirst(car.name),
}) })
}) })
}) })
@@ -453,12 +455,18 @@ describe("joiner-config-builder", () => {
name: model.text(), name: model.text(),
}) })
const car = model.define("car", { const car = model.define(
{ name: "car", tableName: "car" },
{
id: model.id(), id: model.id(),
number_plate: model.text().primaryKey(), number_plate: model.text().primaryKey(),
}) }
)
const linkConfig = buildLinkConfigFromDmlObjects("myService", [user, car]) const linkConfig = buildLinkConfigFromDmlObjects("myService", {
user,
car,
})
expectTypeOf(linkConfig).toMatchTypeOf<{ expectTypeOf(linkConfig).toMatchTypeOf<{
user: { user: {
@@ -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<any, any> & 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
}
@@ -14,3 +14,4 @@ export * from "./medusa-service"
export * from "./migration-scripts" export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder" export * from "./mikro-orm-cli-config-builder"
export * from "./module" export * from "./module"
export * from "./define-link"
@@ -1,4 +1,5 @@
import { import {
IDmlEntity,
JoinerServiceConfigAlias, JoinerServiceConfigAlias,
ModuleJoinerConfig, ModuleJoinerConfig,
PropertyType, PropertyType,
@@ -108,13 +109,14 @@ export function defineJoinerConfig(
} }
if (!primaryKeys && modelDefinitions.size) { if (!primaryKeys && modelDefinitions.size) {
const linkConfig = buildLinkConfigFromDmlObjects(serviceName, [ const linkConfig = buildLinkConfigFromDmlObjects(
...modelDefinitions.values(), serviceName,
]) Object.fromEntries(modelDefinitions)
)
primaryKeys = deduplicate( primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => { Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig) as any[]) return (Object.values(entityLinkConfig as any) as any[])
.filter((linkableConfig) => isObject(linkableConfig)) .filter((linkableConfig) => isObject(linkableConfig))
.map((linkableConfig) => { .map((linkableConfig) => {
return linkableConfig.primaryKey return linkableConfig.primaryKey
@@ -152,7 +154,7 @@ export function defineJoinerConfig(
`${pluralize(camelToSnakeCase(entity.name).toLowerCase())}`, `${pluralize(camelToSnakeCase(entity.name).toLowerCase())}`,
], ],
args: { args: {
entity: entity.name, entity: upperCaseFirst(entity.name),
methodSuffix: pluralize(upperCaseFirst(entity.name)), methodSuffix: pluralize(upperCaseFirst(entity.name)),
}, },
})), })),
@@ -175,6 +177,8 @@ export function defineJoinerConfig(
* test: model.text(), * test: model.text(),
* }) * })
* *
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car])
*
* // output: * // output:
* // { * // {
* // user_id: 'User', * // user_id: 'User',
@@ -213,7 +217,7 @@ export function buildLinkableKeysFromDmlObjects<
if (primaryKeys.length) { if (primaryKeys.length) {
primaryKeys.forEach((primaryKey) => { primaryKeys.forEach((primaryKey) => {
linkableKeys[primaryKey] = model.name linkableKeys[primaryKey] = upperCaseFirst(model.name)
}) })
} }
} }
@@ -250,7 +254,7 @@ export function buildLinkableKeysFromMikroOrmObjects(
* test: model.text(), * test: model.text(),
* }) * })
* *
* const links = buildLinkConfigFromDmlObjects('userService', [user, car]) * const links = buildLinkConfigFromDmlObjects('userService', { user, car })
* *
* // output: * // output:
* // { * // {
@@ -279,19 +283,17 @@ export function buildLinkableKeysFromMikroOrmObjects(
*/ */
export function buildLinkConfigFromDmlObjects< export function buildLinkConfigFromDmlObjects<
const ServiceName extends string, const ServiceName extends string,
const T extends DmlEntity<any, any>[] const T extends Record<string, IDmlEntity<any, any>>
>( >(serviceName: ServiceName, models: T): InfersLinksConfig<ServiceName, T> {
serviceName: ServiceName,
models: T = [] as unknown as T
): InfersLinksConfig<ServiceName, T> {
const linkConfig = {} as InfersLinksConfig<ServiceName, T> const linkConfig = {} as InfersLinksConfig<ServiceName, T>
for (const model of models) { for (const model of Object.values(models) ?? []) {
if (!DmlEntity.isDmlEntity(model)) { if (!DmlEntity.isDmlEntity(model)) {
continue continue
} }
const schema = model.schema const schema = model.schema
// @ts-ignore
const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= { const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= {
toJSON: function () { toJSON: function () {
const linkables = Object.entries(this) const linkables = Object.entries(this)
@@ -322,7 +324,7 @@ export function buildLinkConfigFromDmlObjects<
} }
} }
return linkConfig as InfersLinksConfig<ServiceName, T> & Record<any, any> return linkConfig as InfersLinksConfig<ServiceName, T>
} }
/** /**
@@ -353,9 +353,8 @@ export function MedusaService<
class AbstractModuleService_ { class AbstractModuleService_ {
[MedusaServiceSymbol] = true [MedusaServiceSymbol] = true
static [MedusaServiceModelObjectsSymbol] = Object.values( static [MedusaServiceModelObjectsSymbol] =
entities entities as unknown as MedusaServiceReturnType<
) as unknown as MedusaServiceReturnType<
EntitiesConfig extends { __empty: any } EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities> ? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig : EntitiesConfig
+15 -12
View File
@@ -1,11 +1,10 @@
import { Constructor, ModuleExports } from "@medusajs/types" import { Constructor, IDmlEntity, ModuleExports } from "@medusajs/types"
import { MedusaServiceModelObjectsSymbol } from "./medusa-service" import { MedusaServiceModelObjectsSymbol } from "./medusa-service"
import { import {
buildLinkConfigFromDmlObjects, buildLinkConfigFromDmlObjects,
defineJoinerConfig, defineJoinerConfig,
} from "./joiner-config-builder" } from "./joiner-config-builder"
import { InfersLinksConfig } from "./types/links-config" 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 * 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< export function Module<
const ServiceName extends string, const ServiceName extends string,
const Service extends Constructor<any>, const Service extends Constructor<any>,
const ModelObjects extends DmlEntity<any, any>[] = Service extends { ModelObjects extends Record<string, IDmlEntity<any, any>> = Service extends {
$modelObjects: infer $DmlObjects $modelObjects: any
} }
? $DmlObjects ? Service["$modelObjects"]
: [], : {},
Links = keyof ModelObjects extends never Linkable = keyof ModelObjects extends never
? Record<string, any> ? Record<string, any>
: InfersLinksConfig<ServiceName, ModelObjects> : InfersLinksConfig<ServiceName, ModelObjects>
>( >(
serviceName: ServiceName, serviceName: ServiceName,
{ service, loaders }: ModuleExports<Service> { service, loaders }: ModuleExports<Service>
): ModuleExports<Service> & { ): ModuleExports<Service> & {
links: Links linkable: Linkable
} { } {
service.prototype.__joinerConfig ??= defineJoinerConfig(serviceName) service.prototype.__joinerConfig ??= defineJoinerConfig(serviceName)
const dmlObjects = service[MedusaServiceModelObjectsSymbol] const dmlObjects = service[MedusaServiceModelObjectsSymbol] ?? {}
return { return {
service, service,
loaders, loaders,
links: (dmlObjects?.length linkable: (Object.keys(dmlObjects)?.length
? buildLinkConfigFromDmlObjects<ServiceName, ModelObjects>(dmlObjects) ? buildLinkConfigFromDmlObjects<ServiceName, ModelObjects>(
: {}) as Links, serviceName,
dmlObjects
)
: {}) as Linkable,
} }
} }
@@ -1,10 +1,11 @@
import { import {
DMLSchema, DMLSchema,
IDmlEntity,
IDmlEntityConfig, IDmlEntityConfig,
InferDmlEntityNameFromConfig, InferDmlEntityNameFromConfig,
Prettify,
SnakeCase, SnakeCase,
} from "@medusajs/types" } from "@medusajs/types"
import { DmlEntity } from "../../dml"
import { PrimaryKeyModifier } from "../../dml/properties/primary-key" import { PrimaryKeyModifier } from "../../dml/properties/primary-key"
/** /**
@@ -54,22 +55,22 @@ type InferLinkableKeyName<
string}` string}`
: never : never
type InferSchemaLinkableKeys<T> = T extends DmlEntity< type InferSchemaLinkableKeys<T> = T extends IDmlEntity<
infer Schema, infer Schema,
infer Config infer Config
> >
? { ? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any> [K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferLinkableKeyName<K, Schema[K], Config> ? InferLinkableKeyName<K, Schema[K], Config>
: never]: InferDmlEntityNameFromConfig<Config> : never]: Capitalize<InferDmlEntityNameFromConfig<Config>>
} }
: {} : {}
type InferSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = { type InferSchemasLinkableKeys<T extends IDmlEntity<any, any>[]> = {
[K in keyof T]: InferSchemaLinkableKeys<T[K]> [K in keyof T]: InferSchemaLinkableKeys<T[K]>
} }
type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = { type AggregateSchemasLinkableKeys<T extends IDmlEntity<any, any>[]> = {
[K in keyof InferSchemasLinkableKeys<T>]: InferSchemasLinkableKeys<T>[K] [K in keyof InferSchemasLinkableKeys<T>]: InferSchemasLinkableKeys<T>[K]
} }
@@ -92,7 +93,7 @@ type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' } * const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' }
* *
*/ */
export type InferLinkableKeys<T extends DmlEntity<any, any>[]> = export type InferLinkableKeys<T extends IDmlEntity<any, any>[]> =
UnionToIntersection<FlattenUnion<AggregateSchemasLinkableKeys<T>>[0]> UnionToIntersection<FlattenUnion<AggregateSchemasLinkableKeys<T>>[0]>
/** /**
@@ -141,14 +142,12 @@ type InferPrimaryKeyNameOrNever<
type InferSchemaLinksConfig< type InferSchemaLinksConfig<
ServiceName extends string, ServiceName extends string,
T T
> = T extends DmlEntity<infer Schema, infer Config> > = T extends IDmlEntity<infer Schema, infer Config>
? { ? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any> [K in keyof Schema as InferPrimaryKeyNameOrNever<Schema, K>]: {
? InferPrimaryKeyNameOrNever<Schema, K>
: never]: {
serviceName: ServiceName serviceName: ServiceName
field: T extends DmlEntity<any, infer Config> field: T extends IDmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>> ? InferDmlEntityNameFromConfig<Config>
: string : string
linkable: InferLinkableKeyName<K, Schema[K], Config> linkable: InferLinkableKeyName<K, Schema[K], Config>
primaryKey: K primaryKey: K
@@ -200,20 +199,22 @@ type InferSchemaLinksConfig<
*/ */
export type InfersLinksConfig< export type InfersLinksConfig<
ServiceName extends string, ServiceName extends string,
T extends DmlEntity<any, any>[] T extends Record<string, IDmlEntity<any, any>>
> = UnionToIntersection<{ > = Prettify<{
[K in keyof T as T[K] extends DmlEntity<any, infer Config> [K in keyof T as T[K] extends IDmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>> ? InferDmlEntityNameFromConfig<Config>
: never]: InferSchemaLinksConfig<ServiceName, T[K]> & { : never]: Prettify<
InferSchemaLinksConfig<ServiceName, T[K]> & {
toJSON: () => { toJSON: () => {
serviceName: ServiceName serviceName: ServiceName
field: T[K] extends DmlEntity<any, infer Config> field: T[K] extends IDmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>> ? InferDmlEntityNameFromConfig<Config>
: string : string
linkable: InferLastLinkable<ServiceName, T[K]> linkable: InferLastLinkable<ServiceName, T[K]>
primaryKey: InferLastPrimaryKey<ServiceName, T[K]> primaryKey: InferLastPrimaryKey<ServiceName, T[K]>
} }
} }
>
}> }>
/** /**
@@ -2,6 +2,7 @@ import {
Constructor, Constructor,
Context, Context,
FindConfig, FindConfig,
IDmlEntity,
Pluralize, Pluralize,
RestoreReturn, RestoreReturn,
SoftDeleteReturn, SoftDeleteReturn,
@@ -42,7 +43,7 @@ export type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
model: T[Key] extends { model: infer MODEL } model: T[Key] extends { model: infer MODEL }
? MODEL ? MODEL
: T[Key] extends DmlEntity<any, any> : T[Key] extends IDmlEntity<any, any>
? T[Key] ? T[Key]
: never : never
/** /**
@@ -242,20 +243,20 @@ export type AbstractModuleService<
type InferModelFromConfig<T> = { type InferModelFromConfig<T> = {
[K in keyof T as T[K] extends { model: any } [K in keyof T as T[K] extends { model: any }
? K ? K
: K extends DmlEntity<any, any> : K extends IDmlEntity<any, any>
? K ? K
: never]: T[K] extends { : never]: T[K] extends {
model: infer MODEL model: infer MODEL
} }
? MODEL extends DmlEntity<any, any> ? MODEL extends IDmlEntity<any, any>
? MODEL ? MODEL
: never : never
: T[K] extends DmlEntity<any, any> : T[K] extends IDmlEntity<any, any>
? T[K] ? T[K]
: never : never
} }
export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = { export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = {
new (...args: any[]): AbstractModuleService<ModelsConfig> new (...args: any[]): AbstractModuleService<ModelsConfig>
$modelObjects: InferModelFromConfig<ModelsConfig>[keyof InferModelFromConfig<ModelsConfig>][] $modelObjects: InferModelFromConfig<ModelsConfig>
} }
+3 -4
View File
@@ -1,9 +1,8 @@
import { ModuleExports } from "@medusajs/types"
import { RegionModuleService } from "./services" import { RegionModuleService } from "./services"
import loadDefaults from "./loaders/defaults" import loadDefaults from "./loaders/defaults"
import { Module, Modules } from "@medusajs/utils"
const moduleDefinition: ModuleExports = { export default Module(Modules.REGION, {
service: RegionModuleService, service: RegionModuleService,
loaders: [loadDefaults], loaders: [loadDefaults],
} })
export default moduleDefinition
@@ -1,13 +1,13 @@
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { ContainerRegistrationKeys, DefaultsUtils } from "@medusajs/utils" import { ContainerRegistrationKeys, DefaultsUtils } from "@medusajs/utils"
import { RegionCountry } from "@models" import { Country } from "@models"
export default async ({ container }: LoaderOptions): Promise<void> => { export default async ({ container }: LoaderOptions): Promise<void> => {
// TODO: Add default logger to the container when running tests // TODO: Add default logger to the container when running tests
const logger = const logger =
container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console container.resolve<Logger>(ContainerRegistrationKeys.LOGGER) ?? console
const countryService_: ModulesSdkTypes.IMedusaInternalService< const countryService_: ModulesSdkTypes.IMedusaInternalService<
typeof RegionCountry typeof Country
> = container.resolve("countryService") > = container.resolve("countryService")
try { try {
@@ -1,7 +1,7 @@
import { model } from "@medusajs/utils" import { model } from "@medusajs/utils"
import Region from "./region" import Region from "./region"
const Country = model export default model
.define( .define(
{ name: "Country", tableName: "region_country" }, { name: "Country", tableName: "region_country" },
{ {
@@ -24,5 +24,3 @@ const Country = model
unique: true, unique: true,
}, },
]) ])
export default Country
+1 -2
View File
@@ -1,3 +1,2 @@
export { default as RegionCountry } from "./country" export { default as Country } from "./country"
export { default as Region } from "./region" export { default as Region } from "./region"
+1 -3
View File
@@ -1,7 +1,7 @@
import { model } from "@medusajs/utils" import { model } from "@medusajs/utils"
import RegionCountry from "./country" import RegionCountry from "./country"
const Region = model.define("region", { export default model.define("region", {
id: model.id({ prefix: "reg" }).primaryKey(), id: model.id({ prefix: "reg" }).primaryKey(),
name: model.text().searchable(), name: model.text().searchable(),
currency_code: model.text().searchable(), currency_code: model.text().searchable(),
@@ -9,5 +9,3 @@ const Region = model.define("region", {
countries: model.hasMany(() => RegionCountry), countries: model.hasMany(() => RegionCountry),
metadata: model.json().nullable(), metadata: model.json().nullable(),
}) })
export default Region
@@ -26,7 +26,7 @@ import {
promiseAll, promiseAll,
removeUndefined, removeUndefined,
} from "@medusajs/utils" } from "@medusajs/utils"
import { Region, RegionCountry as Country } from "@models" import { Country, Region } from "@models"
import { UpdateRegionInput } from "@types" import { UpdateRegionInput } from "@types"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
@@ -40,9 +40,11 @@ export default class RegionModuleService
extends MedusaService<{ extends MedusaService<{
Region: { Region: {
dto: RegionDTO dto: RegionDTO
model: typeof Region
} }
Country: { Country: {
dto: RegionCountryDTO dto: RegionCountryDTO
model: typeof Country
} }
}>({ Region, Country }) }>({ Region, Country })
implements IRegionModuleService implements IRegionModuleService