Files
medusa-store/packages/core/modules-sdk/src/medusa-app.ts
2024-05-31 15:22:03 +05:30

501 lines
13 KiB
TypeScript

import type { Knex } from "knex"
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
import type {
ConfigModule,
ExternalModuleDeclaration,
InternalModuleDeclaration,
LoadedModule,
Logger,
MedusaContainer,
ModuleDefinition,
ModuleExports,
ModuleJoinerConfig,
ModuleServiceInitializeOptions,
RemoteJoinerOptions,
RemoteJoinerQuery,
RemoteQueryFunction,
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
ModulesSdkUtils,
createMedusaContainer,
isObject,
isString,
promiseAll,
} from "@medusajs/utils"
import { asValue } from "awilix"
import {
MODULE_PACKAGE_NAMES,
ModuleRegistrationName,
Modules,
} from "./definitions"
import { MedusaModule } from "./medusa-module"
import { RemoteLink } from "./remote-link"
import { RemoteQuery } from "./remote-query"
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
import { cleanGraphQLSchema } from "./utils"
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
declare module "@medusajs/types" {
export interface ModuleImplementations {
[ContainerRegistrationKeys.REMOTE_LINK]: RemoteLink
[ContainerRegistrationKeys.CONFIG_MODULE]: ConfigModule
[ContainerRegistrationKeys.PG_CONNECTION]: Knex<any>
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
[ContainerRegistrationKeys.LOGGER]: Logger
}
}
export type RunMigrationFn = (
options?: ModuleServiceInitializeOptions,
injectedDependencies?: Record<any, any>
) => Promise<void>
export type MedusaModuleConfig = {
[key: string | Modules]:
| string
| boolean
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
}
export type SharedResources = {
database?: ModuleServiceInitializeOptions["database"] & {
/**
* {
* name?: string
* afterCreate?: Function
* min?: number
* max?: number
* refreshIdle?: boolean
* idleTimeoutMillis?: number
* reapIntervalMillis?: number
* returnToHead?: boolean
* priorityRange?: number
* log?: (message: string, logLevel: string) => void
* }
*/
pool?: Record<string, unknown>
}
}
export async function loadModules(
modulesConfig,
sharedContainer,
migrationOnly = false,
loaderOnly = false,
workerMode: "shared" | "worker" | "server" = "server"
) {
const allModules = {}
await Promise.all(
Object.keys(modulesConfig).map(async (moduleName) => {
const mod = modulesConfig[moduleName]
let path: string
let moduleExports: ModuleExports | undefined = undefined
let declaration: any = {}
let definition: Partial<ModuleDefinition> | undefined = undefined
if (isObject(mod)) {
const mod_ = mod as unknown as InternalModuleDeclaration
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
definition = mod_.definition
moduleExports = !isString(mod_.resolve)
? (mod_.resolve as ModuleExports)
: undefined
declaration = { ...mod }
delete declaration.definition
} else {
path = MODULE_PACKAGE_NAMES[moduleName]
}
declaration.scope ??= MODULE_SCOPE.INTERNAL
if (
declaration.scope === MODULE_SCOPE.INTERNAL &&
!declaration.resources
) {
declaration.resources = MODULE_RESOURCE_TYPE.SHARED
}
const loaded = (await MedusaModule.bootstrap({
moduleKey: moduleName,
defaultPath: path,
declaration,
sharedContainer,
moduleDefinition: definition as ModuleDefinition,
moduleExports,
migrationOnly,
loaderOnly,
workerMode,
})) as LoadedModule
if (loaderOnly) {
return
}
const service = loaded[moduleName]
sharedContainer.register({
[service.__definition.registrationName]: asValue(service),
})
if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) {
allModules[moduleName] = []
}
if (allModules[moduleName]) {
;(allModules[moduleName] as LoadedModule[]).push(loaded[moduleName])
} else {
allModules[moduleName] = loaded[moduleName]
}
})
)
return allModules
}
async function initializeLinks({
config,
linkModules,
injectedDependencies,
moduleExports,
}) {
try {
const { initialize, runMigrations, revertMigrations } =
moduleExports ?? (await import(LinkModulePackage))
const linkResolution = await initialize(
config,
linkModules,
injectedDependencies
)
return {
remoteLink: new RemoteLink(),
linkResolution,
runMigrations,
revertMigrations,
}
} catch (err) {
console.warn("Error initializing link modules.", err)
return {
remoteLink: undefined,
linkResolution: undefined,
runMigrations: undefined,
}
}
}
function isMedusaModule(mod) {
return typeof mod?.initialize === "function"
}
function cleanAndMergeSchema(loadedSchema) {
const defaultMedusaSchema = `
scalar DateTime
scalar JSON
`
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(
defaultMedusaSchema + loadedSchema
)
const mergedSchema = mergeTypeDefs(cleanedSchema)
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
}
function getLoadedSchema(): string {
return MedusaModule.getAllJoinerConfigs()
.map((joinerConfig) => joinerConfig?.schema ?? "")
.join("\n")
}
function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) {
for (const config of servicesConfig) {
if (!config.serviceName || config.isReadOnlyLink) {
continue
}
MedusaModule.setJoinerConfig(config.serviceName, config)
}
}
export type MedusaAppOutput = {
modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined
query: RemoteQueryFunction
entitiesMap?: Record<string, any>
notFound?: Record<string, Record<string, string>>
runMigrations: RunMigrationFn
revertMigrations: RunMigrationFn
onApplicationShutdown: () => Promise<void>
onApplicationPrepareShutdown: () => Promise<void>
}
export type MedusaAppOptions = {
workerMode?: "shared" | "worker" | "server"
sharedContainer?: MedusaContainer
sharedResourcesConfig?: SharedResources
loadedModules?: LoadedModule[]
servicesConfig?: ModuleJoinerConfig[]
modulesConfigPath?: string
modulesConfigFileName?: string
modulesConfig?: MedusaModuleConfig
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback
injectedDependencies?: any
onApplicationStartCb?: () => void
/**
* Forces the modules bootstrapper to only run the modules loaders and return prematurely
*/
loaderOnly?: boolean
}
async function MedusaApp_({
sharedContainer,
sharedResourcesConfig,
servicesConfig,
modulesConfigPath,
modulesConfigFileName,
modulesConfig,
linkModules,
remoteFetchData,
injectedDependencies = {},
onApplicationStartCb,
migrationOnly = false,
loaderOnly = false,
workerMode = "server",
}: MedusaAppOptions & {
migrationOnly?: boolean
} = {}): Promise<MedusaAppOutput> {
const sharedContainer_ = createMedusaContainer({}, sharedContainer)
const onApplicationShutdown = async () => {
await promiseAll([
MedusaModule.onApplicationShutdown(),
sharedContainer_.dispose(),
])
}
const onApplicationPrepareShutdown = async () => {
await promiseAll([MedusaModule.onApplicationPrepareShutdown()])
}
const modules: MedusaModuleConfig =
modulesConfig ??
(
await import(
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default
const dbData = ModulesSdkUtils.loadDatabaseConfig(
"medusa",
sharedResourcesConfig as ModuleServiceInitializeOptions,
true
)!
registerCustomJoinerConfigs(servicesConfig ?? [])
if (
sharedResourcesConfig?.database?.connection &&
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
) {
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
sharedResourcesConfig.database.connection
} else if (
dbData.clientUrl &&
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
) {
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
ModulesSdkUtils.createPgConnection({
...(sharedResourcesConfig?.database ?? {}),
...dbData,
})
}
// remove the link module from the modules
const linkModule = modules[LinkModulePackage] ?? modules[Modules.LINK]
delete modules[LinkModulePackage]
delete modules[Modules.LINK]
let linkModuleOptions = {}
if (isObject(linkModule)) {
linkModuleOptions = linkModule
}
for (const injectedDependency of Object.keys(injectedDependencies)) {
sharedContainer_.register({
[injectedDependency]: asValue(injectedDependencies[injectedDependency]),
})
}
const allModules = await loadModules(
modules,
sharedContainer_,
migrationOnly,
loaderOnly,
workerMode
)
if (loaderOnly) {
return {
onApplicationShutdown,
onApplicationPrepareShutdown,
modules: allModules,
link: undefined,
query: async () => {
throw new Error("Querying not allowed in loaderOnly mode")
},
runMigrations: async () => {
throw new Error("Migrations not allowed in loaderOnly mode")
},
revertMigrations: async () => {
throw new Error("Revert migrations not allowed in loaderOnly mode")
},
}
}
// Share Event bus with link modules
injectedDependencies[ModuleRegistrationName.EVENT_BUS] =
sharedContainer_.resolve(ModuleRegistrationName.EVENT_BUS, {
allowUnregistered: true,
})
const {
remoteLink,
runMigrations: linkModuleMigration,
revertMigrations: revertLinkModuleMigration,
} = await initializeLinks({
config: linkModuleOptions,
linkModules,
injectedDependencies,
moduleExports: isMedusaModule(linkModule) ? linkModule : undefined,
})
const loadedSchema = getLoadedSchema()
const { schema, notFound } = cleanAndMergeSchema(loadedSchema)
const remoteQuery = new RemoteQuery({
servicesConfig,
customRemoteFetchData: remoteFetchData,
})
const query = async (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>,
options?: RemoteJoinerOptions
) => {
return await remoteQuery.query(query, variables, options)
}
const applyMigration = async (linkModuleOptions, revert = false) => {
for (const moduleName of Object.keys(allModules)) {
const moduleResolution = MedusaModule.getModuleResolutions(moduleName)
if (!moduleResolution.options?.database) {
moduleResolution.options ??= {}
moduleResolution.options.database = {
...(sharedResourcesConfig?.database ?? {}),
}
}
if (revert) {
await MedusaModule.migrateDown(
moduleResolution.definition.key,
moduleResolution.resolutionPath as string,
moduleResolution.options,
moduleResolution.moduleExports
)
} else {
await MedusaModule.migrateUp(
moduleResolution.definition.key,
moduleResolution.resolutionPath as string,
moduleResolution.options,
moduleResolution.moduleExports
)
}
}
const linkModuleOpt = { ...(linkModuleOptions ?? {}) }
linkModuleOpt.database ??= {
...(sharedResourcesConfig?.database ?? {}),
}
if (revert) {
revertLinkModuleMigration &&
(await revertLinkModuleMigration({
options: linkModuleOpt,
injectedDependencies,
}))
} else {
linkModuleMigration &&
(await linkModuleMigration({
options: linkModuleOpt,
injectedDependencies,
}))
}
}
const runMigrations: RunMigrationFn = async (
linkModuleOptions
): Promise<void> => {
await applyMigration(linkModuleOptions)
}
const revertMigrations: RunMigrationFn = async (
linkModuleOptions
): Promise<void> => {
await applyMigration(linkModuleOptions, true)
}
return {
onApplicationShutdown,
onApplicationPrepareShutdown,
modules: allModules,
link: remoteLink,
query,
entitiesMap: schema.getTypeMap(),
notFound,
runMigrations,
revertMigrations,
}
}
export async function MedusaApp(
options: MedusaAppOptions = {}
): Promise<MedusaAppOutput> {
try {
return await MedusaApp_(options)
} finally {
MedusaModule.onApplicationStart(options.onApplicationStartCb)
}
}
export async function MedusaAppMigrateUp(
options: MedusaAppOptions = {}
): Promise<void> {
const migrationOnly = true
const { runMigrations } = await MedusaApp_({
...options,
migrationOnly,
})
await runMigrations().finally(MedusaModule.clearInstances)
}
export async function MedusaAppMigrateDown(
options: MedusaAppOptions = {}
): Promise<void> {
const migrationOnly = true
const { revertMigrations } = await MedusaApp_({
...options,
migrationOnly,
})
await revertMigrations().finally(MedusaModule.clearInstances)
}