diff --git a/.changeset/long-waves-itch.md b/.changeset/long-waves-itch.md new file mode 100644 index 0000000000..7d949c2eb1 --- /dev/null +++ b/.changeset/long-waves-itch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/modules-sdk": patch +--- + +defineLink helper - MedusaApp loading registered links diff --git a/packages/core/modules-sdk/src/index.ts b/packages/core/modules-sdk/src/index.ts index 5df4a1e178..fe00aabcc7 100644 --- a/packages/core/modules-sdk/src/index.ts +++ b/packages/core/modules-sdk/src/index.ts @@ -6,3 +6,4 @@ 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/medusa-app.ts b/packages/core/modules-sdk/src/medusa-app.ts index 4ea6285633..ccf614874d 100644 --- a/packages/core/modules-sdk/src/medusa-app.ts +++ b/packages/core/modules-sdk/src/medusa-app.ts @@ -1,4 +1,3 @@ -import type { Knex } from "knex" import { mergeTypeDefs } from "@graphql-tools/merge" import { makeExecutableSchema } from "@graphql-tools/schema" import { RemoteFetchDataCallback } from "@medusajs/orchestration" @@ -26,12 +25,13 @@ import { promiseAll, } from "@medusajs/utils" import { asValue } from "awilix" +import type { Knex } from "knex" import { MODULE_PACKAGE_NAMES, ModuleRegistrationName, Modules, } from "./definitions" -import { MedusaModule } from "./medusa-module" +import { MedusaModule, RegisterModuleJoinerConfig } from "./medusa-module" import { RemoteLink } from "./remote-link" import { RemoteQuery } from "./remote-query" import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types" @@ -240,7 +240,7 @@ export type MedusaAppOptions = { modulesConfigPath?: string modulesConfigFileName?: string modulesConfig?: MedusaModuleConfig - linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[] + linkModules?: RegisterModuleJoinerConfig | RegisterModuleJoinerConfig[] remoteFetchData?: RemoteFetchDataCallback injectedDependencies?: any onApplicationStartCb?: () => void @@ -260,7 +260,6 @@ async function MedusaApp_({ linkModules, remoteFetchData, injectedDependencies = {}, - onApplicationStartCb, migrationOnly = false, loaderOnly = false, workerMode = "server", @@ -363,6 +362,20 @@ async function MedusaApp_({ allowUnregistered: true, }) + linkModules ??= [] + if (!Array.isArray(linkModules)) { + linkModules = [linkModules] + } + linkModules.push(...MedusaModule.getCustomLinks()) + + const allLoadedJoinerConfigs = MedusaModule.getAllJoinerConfigs() + for (let linkIdx = 0; linkIdx < linkModules.length; linkIdx++) { + const customLink: any = linkModules[linkIdx] + if (typeof customLink === "function") { + linkModules[linkIdx] = customLink(allLoadedJoinerConfigs) + } + } + const { remoteLink, runMigrations: linkModuleMigration, diff --git a/packages/core/modules-sdk/src/medusa-module.ts b/packages/core/modules-sdk/src/medusa-module.ts index 664784f4a6..557879a71e 100644 --- a/packages/core/modules-sdk/src/medusa-module.ts +++ b/packages/core/modules-sdk/src/medusa-module.ts @@ -80,10 +80,15 @@ export type LinkModuleBootstrapOptions = { injectedDependencies?: Record } +export type RegisterModuleJoinerConfig = + | ModuleJoinerConfig + | ((modules: ModuleJoinerConfig[]) => ModuleJoinerConfig) + export class MedusaModule { private static instances_: Map = new Map() private static modules_: Map = new Map() + private static customLinks_: RegisterModuleJoinerConfig[] = [] private static loading_: Map> = new Map() private static joinerConfig_: Map = new Map() private static moduleResolutions_: Map = new Map() @@ -203,6 +208,14 @@ export class MedusaModule { return config } + public static setCustomLink(config: RegisterModuleJoinerConfig): void { + MedusaModule.customLinks_.push(config) + } + + public static getCustomLinks(): RegisterModuleJoinerConfig[] { + return MedusaModule.customLinks_ + } + public static getModuleInstance( moduleKey: string, alias?: string diff --git a/packages/core/modules-sdk/src/utils/define-link.ts b/packages/core/modules-sdk/src/utils/define-link.ts new file mode 100644 index 0000000000..5e1f6d796c --- /dev/null +++ b/packages/core/modules-sdk/src/utils/define-link.ts @@ -0,0 +1,232 @@ +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 d1470bdcbc..ff694c9a3f 100644 --- a/packages/core/modules-sdk/src/utils/index.ts +++ b/packages/core/modules-sdk/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./clean-graphql-schema" +export * from "./define-link" export * from "./graphql-schema-to-fields" diff --git a/packages/core/types/src/modules-sdk/index.ts b/packages/core/types/src/modules-sdk/index.ts index 3e868e6a2a..00a5756ac4 100644 --- a/packages/core/types/src/modules-sdk/index.ts +++ b/packages/core/types/src/modules-sdk/index.ts @@ -129,28 +129,39 @@ export type ModulesResponse = { resolution: string | false }[] -type ExtraFieldType = - | "date" - | "time" - | "datetime" - | "bigint" - | "blob" - | "uint8array" - | "array" - | "enumArray" - | "enum" - | "json" - | "integer" - | "smallint" - | "tinyint" - | "mediumint" - | "float" - | "double" - | "boolean" - | "decimal" - | "string" - | "uuid" - | "text" +export type LinkModulesExtraFields = Record< + string, + { + type: + | "date" + | "time" + | "datetime" + | "bigint" + | "blob" + | "uint8array" + | "array" + | "enumArray" + | "enum" + | "json" + | "integer" + | "smallint" + | "tinyint" + | "mediumint" + | "float" + | "double" + | "boolean" + | "decimal" + | "string" + | "uuid" + | "text" + defaultValue?: string + nullable?: boolean + /** + * Mikro-orm options for the column + */ + options?: Record + } +> export type ModuleJoinerConfig = Omit< JoinerServiceConfig, @@ -202,18 +213,7 @@ export type ModuleJoinerConfig = Omit< * Prefix for the id column. If not provided it is "link" */ idPrefix?: string - extraFields?: Record< - string, - { - type: ExtraFieldType - defaultValue?: string - nullable?: boolean - /** - * Mikro-orm options for the column - */ - options?: Record - } - > + extraFields?: LinkModulesExtraFields } }