chore(): Autoload module resources (#7291)
**What** - automatically build and consume connection and container loader if not exported by the module - therefore load the services and repositories automatically, including baseRepository - automatically build run and revert migrations if not provided - cleaup modules to remove extra unnecessary bits and pieces - remove the `initializeFactory` in favor of using `medusaApp` Should drastically improve the module building DX by removing a lot of boilerplate to handle by the user, that plus the base entity should simplify quite a lot the flow cc @shahednasser **Note** I had to choose a way to identify connection and container loader from the exported loader from the module. I decided to go with named function `connectionLoader` and `containerLoader`, also, now the factories will return named function so if the user use the factories we are providing to build those loaders, the function will also be named and identified
This commit is contained in:
@@ -5,4 +5,3 @@ export * from "./medusa-app"
|
||||
export * from "./medusa-module"
|
||||
export * from "./remote-link"
|
||||
export * from "./remote-query"
|
||||
export * from "./utils/initialize-factory"
|
||||
|
||||
@@ -137,11 +137,9 @@ function getInternalModuleResolution(
|
||||
// If user added a module and it's overridable, we resolve that instead
|
||||
const isStr = isString(moduleConfig)
|
||||
if (isStr || (isObj && moduleConfig.resolve)) {
|
||||
resolutionPath = !moduleExports
|
||||
? resolveCwd(isStr ? moduleConfig : (moduleConfig.resolve as string))
|
||||
: // Explicitly assign an empty string, later, we will check if the value is exactly false.
|
||||
// This allows to continue the module loading while using the module exports instead of re importing the module itself during the process.
|
||||
""
|
||||
resolutionPath = resolveCwd(
|
||||
isStr ? moduleConfig : (moduleConfig.resolve as string)
|
||||
)
|
||||
}
|
||||
|
||||
const moduleDeclaration = isObj ? moduleConfig : {}
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import {
|
||||
Constructor,
|
||||
InternalModuleDeclaration,
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
ModuleExports,
|
||||
ModuleLoaderFunction,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
MedusaModuleType,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { asFunction, asValue } from "awilix"
|
||||
import { join, resolve } from "path"
|
||||
import { statSync } from "fs"
|
||||
import { readdir } from "fs/promises"
|
||||
|
||||
type ModuleResource = {
|
||||
services: Function[]
|
||||
models: Function[]
|
||||
repositories: Function[]
|
||||
loaders: ModuleLoaderFunction[]
|
||||
moduleService: Constructor<any>
|
||||
normalizedPath: string
|
||||
}
|
||||
|
||||
export async function loadInternalModule(
|
||||
container: MedusaContainer,
|
||||
@@ -36,6 +51,8 @@ export async function loadInternalModule(
|
||||
const modulePath = resolution.resolutionPath as string
|
||||
|
||||
if (resolution.moduleExports) {
|
||||
// TODO:
|
||||
// If we want to benefit from the auto load mechanism, even if the module exports is provided, we need to ask for the module path
|
||||
loadedModule = resolution.moduleExports
|
||||
} else {
|
||||
loadedModule = await import(modulePath)
|
||||
@@ -56,7 +73,17 @@ export async function loadInternalModule(
|
||||
return { error }
|
||||
}
|
||||
|
||||
if (!loadedModule?.service) {
|
||||
let moduleResources = {} as ModuleResource
|
||||
|
||||
if (resolution.resolutionPath) {
|
||||
moduleResources = await loadResources(
|
||||
loadedModule?.loaders ?? [],
|
||||
resolution,
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
if (!loadedModule?.service && !moduleResources.moduleService) {
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
@@ -69,9 +96,11 @@ export async function loadInternalModule(
|
||||
}
|
||||
|
||||
if (migrationOnly) {
|
||||
const moduleService_ = moduleResources.moduleService ?? loadedModule.service
|
||||
|
||||
// Partially loaded module, only register the service __joinerConfig function to be able to resolve it later
|
||||
const moduleService = {
|
||||
__joinerConfig: loadedModule.service.prototype.__joinerConfig,
|
||||
__joinerConfig: moduleService_.prototype.__joinerConfig,
|
||||
}
|
||||
container.register({
|
||||
[registrationName]: asValue(moduleService),
|
||||
@@ -100,32 +129,21 @@ export async function loadInternalModule(
|
||||
)
|
||||
}
|
||||
|
||||
const moduleLoaders = loadedModule?.loaders ?? []
|
||||
try {
|
||||
for (const loader of moduleLoaders) {
|
||||
await loader(
|
||||
{
|
||||
container: localContainer,
|
||||
logger,
|
||||
options: resolution.options,
|
||||
dataLoaderOnly: loaderOnly,
|
||||
},
|
||||
resolution.moduleDeclaration as InternalModuleDeclaration
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
const loaders = moduleResources.loaders ?? loadedModule?.loaders ?? []
|
||||
const error = await runLoaders(loaders, {
|
||||
container,
|
||||
localContainer,
|
||||
logger,
|
||||
resolution,
|
||||
loaderOnly,
|
||||
registrationName,
|
||||
})
|
||||
|
||||
return {
|
||||
error: new Error(
|
||||
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
|
||||
),
|
||||
}
|
||||
if (error) {
|
||||
return error
|
||||
}
|
||||
|
||||
const moduleService = loadedModule.service
|
||||
const moduleService = moduleResources.moduleService ?? loadedModule.service
|
||||
|
||||
container.register({
|
||||
[registrationName]: asFunction((cradle) => {
|
||||
@@ -155,8 +173,229 @@ export async function loadModuleMigrations(
|
||||
loadedModule =
|
||||
moduleExports ?? (await import(resolution.resolutionPath as string))
|
||||
|
||||
return [loadedModule.runMigrations, loadedModule.revertMigration]
|
||||
let runMigrations = loadedModule.runMigrations
|
||||
let revertMigration = loadedModule.revertMigration
|
||||
|
||||
// Generate migration scripts if they are not present
|
||||
if (!runMigrations || !revertMigration) {
|
||||
const moduleResources = await loadResources(
|
||||
loadedModule?.loaders ?? [],
|
||||
resolution,
|
||||
console as unknown as Logger
|
||||
)
|
||||
|
||||
const migrationScriptOptions = {
|
||||
moduleName: resolution.definition.key,
|
||||
models: moduleResources.models,
|
||||
pathToMigrations: moduleResources.normalizedPath + "/dist/migrations",
|
||||
}
|
||||
|
||||
runMigrations ??= ModulesSdkUtils.buildMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
|
||||
revertMigration ??= ModulesSdkUtils.buildRevertMigrationScript(
|
||||
migrationScriptOptions
|
||||
)
|
||||
}
|
||||
|
||||
return [runMigrations, revertMigration]
|
||||
} catch {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
}
|
||||
|
||||
async function importAllFromDir(path: string) {
|
||||
let filesToLoad: string[] = []
|
||||
|
||||
await readdir(path).then((files) => {
|
||||
files.forEach((file) => {
|
||||
if (file !== "index.js" && file.endsWith(".js")) {
|
||||
const filePath = join(path, file)
|
||||
const stats = statSync(filePath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
// TODO: should we handle that? dont think so but I put that here for discussion
|
||||
} else if (stats.isFile()) {
|
||||
filesToLoad.push(filePath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return filesToLoad
|
||||
})
|
||||
|
||||
return (
|
||||
await Promise.all(filesToLoad.map((filePath) => import(filePath)))
|
||||
).flatMap((value) => {
|
||||
return Object.values(value)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadResources(
|
||||
loadedModuleLoaders: ModuleLoaderFunction[],
|
||||
moduleResolution: ModuleResolution,
|
||||
logger: Logger
|
||||
): Promise<ModuleResource> {
|
||||
const modulePath = moduleResolution.resolutionPath as string
|
||||
let normalizedPath = modulePath.replace("dist/", "").replace("index.js", "")
|
||||
normalizedPath = resolve(normalizedPath)
|
||||
|
||||
try {
|
||||
const defaultOnFail = () => {
|
||||
return []
|
||||
}
|
||||
|
||||
const [moduleService, services, models, repositories] = await Promise.all([
|
||||
import(modulePath).then((moduleExports) => moduleExports.default.service),
|
||||
importAllFromDir(resolve(normalizedPath, "dist", "services")).catch(
|
||||
defaultOnFail
|
||||
),
|
||||
importAllFromDir(resolve(normalizedPath, "dist", "models")).catch(
|
||||
defaultOnFail
|
||||
),
|
||||
importAllFromDir(resolve(normalizedPath, "dist", "repositories")).catch(
|
||||
defaultOnFail
|
||||
),
|
||||
])
|
||||
|
||||
const cleanupResources = (resources) => {
|
||||
return Object.values(resources).filter(
|
||||
(resource): resource is Function => {
|
||||
return typeof resource === "function"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const potentialServices = [...new Set(cleanupResources(services))]
|
||||
const potentialModels = [...new Set(cleanupResources(models))]
|
||||
const potentialRepositories = [...new Set(cleanupResources(repositories))]
|
||||
|
||||
const finalLoaders = prepareLoaders({
|
||||
loadedModuleLoaders,
|
||||
models: potentialModels,
|
||||
repositories: potentialRepositories,
|
||||
services: potentialServices,
|
||||
moduleResolution,
|
||||
migrationPath: normalizedPath + "/dist/migrations",
|
||||
})
|
||||
|
||||
return {
|
||||
services: potentialServices,
|
||||
models: potentialModels,
|
||||
repositories: potentialRepositories,
|
||||
loaders: finalLoaders,
|
||||
moduleService,
|
||||
normalizedPath
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Unable to load resources for module ${modulePath} automagically. ${e.message}`
|
||||
)
|
||||
|
||||
return {} as ModuleResource
|
||||
}
|
||||
}
|
||||
|
||||
async function runLoaders(
|
||||
loaders: Function[] = [],
|
||||
{
|
||||
localContainer,
|
||||
container,
|
||||
logger,
|
||||
resolution,
|
||||
loaderOnly,
|
||||
registrationName,
|
||||
}
|
||||
): Promise<void | { error: Error }> {
|
||||
try {
|
||||
for (const loader of loaders) {
|
||||
await loader(
|
||||
{
|
||||
container: localContainer,
|
||||
logger,
|
||||
options: resolution.options,
|
||||
dataLoaderOnly: loaderOnly,
|
||||
},
|
||||
resolution.moduleDeclaration as InternalModuleDeclaration
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
|
||||
return {
|
||||
error: new Error(
|
||||
`Loaders for module ${resolution.definition.label} failed: ${err.message}`
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepareLoaders({
|
||||
loadedModuleLoaders,
|
||||
models,
|
||||
repositories,
|
||||
services,
|
||||
moduleResolution,
|
||||
migrationPath,
|
||||
}) {
|
||||
const finalLoaders: ModuleLoaderFunction[] = []
|
||||
|
||||
const toObjectReducer = (acc, curr) => {
|
||||
acc[curr.name] = curr
|
||||
return acc
|
||||
}
|
||||
|
||||
/*
|
||||
* If no connectionLoader function is provided, create a default connection loader.
|
||||
* TODO: Validate naming convention
|
||||
*/
|
||||
const connectionLoaderName = "connectionLoader"
|
||||
const containerLoader = "containerLoader"
|
||||
|
||||
const hasConnectionLoader = loadedModuleLoaders.some(
|
||||
(l) => l.name === connectionLoaderName
|
||||
)
|
||||
|
||||
if (!hasConnectionLoader && models.length > 0) {
|
||||
const connectionLoader = ModulesSdkUtils.mikroOrmConnectionLoaderFactory({
|
||||
moduleName: moduleResolution.definition.key,
|
||||
moduleModels: models,
|
||||
migrationsPath: migrationPath, //normalizedPath + "/dist/migrations",
|
||||
})
|
||||
finalLoaders.push(connectionLoader)
|
||||
}
|
||||
|
||||
const hasContainerLoader = loadedModuleLoaders.some(
|
||||
(l) => l.name === containerLoader
|
||||
)
|
||||
|
||||
if (!hasContainerLoader) {
|
||||
const containerLoader = ModulesSdkUtils.moduleContainerLoaderFactory({
|
||||
moduleModels: models.reduce(toObjectReducer, {}),
|
||||
moduleRepositories: repositories.reduce(toObjectReducer, {}),
|
||||
moduleServices: services.reduce(toObjectReducer, {}),
|
||||
})
|
||||
finalLoaders.push(containerLoader)
|
||||
}
|
||||
|
||||
finalLoaders.push(
|
||||
...loadedModuleLoaders.filter((loader) => {
|
||||
if (
|
||||
loader.name !== connectionLoaderName &&
|
||||
loader.name !== containerLoader
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
(loader.name === containerLoader && hasContainerLoader) ||
|
||||
(loader.name === connectionLoaderName && hasConnectionLoader)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return finalLoaders
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./clean-graphql-schema"
|
||||
export * from "./graphql-schema-to-fields"
|
||||
export * from "./initialize-factory"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
ModuleExports,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/types"
|
||||
import { MODULE_PACKAGE_NAMES } from "../definitions"
|
||||
import { MedusaModule } from "../medusa-module"
|
||||
|
||||
/**
|
||||
* Generate a initialize module factory that is exported by the module to be initialized manually
|
||||
*
|
||||
* @param moduleName
|
||||
* @param moduleDefinition
|
||||
*/
|
||||
export function initializeFactory<T>({
|
||||
moduleName,
|
||||
moduleDefinition,
|
||||
}: {
|
||||
moduleName: string
|
||||
moduleDefinition: ModuleExports
|
||||
}) {
|
||||
return async (
|
||||
options?:
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
injectedDependencies?: any
|
||||
) => {
|
||||
const loaded = await MedusaModule.bootstrap<T>({
|
||||
moduleKey: moduleName,
|
||||
defaultPath: MODULE_PACKAGE_NAMES[moduleName],
|
||||
declaration: options as
|
||||
| InternalModuleDeclaration
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies,
|
||||
moduleExports: moduleDefinition,
|
||||
})
|
||||
|
||||
return loaded[moduleName] as T
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user