feat(medusa): Modules initializer (#3352)

This commit is contained in:
Carlos R. L. Rodrigues
2023-03-17 12:18:52 -03:00
committed by GitHub
parent 8a7421db5b
commit aa690beed7
51 changed files with 1290 additions and 715 deletions

View File

@@ -1,4 +1,5 @@
export * from "./definitions"
export * from "./loaders"
export * from "./medusa-module"
export * from "./module-helper"
export * from "./types"

View File

@@ -1,6 +1,6 @@
import { EOL } from "os"
import { AwilixContainer, ClassOrFunctionReturning, Resolver } from "awilix"
import { createMedusaContainer } from "medusa-core-utils"
import { EOL } from "os"
import {
ModuleResolution,
MODULE_RESOURCE_TYPE,
@@ -155,7 +155,7 @@ describe("modules loader", () => {
await moduleLoader({ container, moduleResolutions, logger })
expect(logger.warn).toHaveBeenCalledWith(
`Could not resolve module: TestService. Error: No service found in module. Make sure your module exports at least one service.${EOL}`
`Could not resolve module: TestService. Error: No service found in module. Make sure your module exports a service.${EOL}`
)
})
@@ -186,7 +186,7 @@ describe("modules loader", () => {
await moduleLoader({ container, moduleResolutions, logger })
} catch (err) {
expect(err.message).toEqual(
"No service found in module. Make sure your module exports at least one service."
"No service found in module. Make sure your module exports a service."
)
}
})

View File

@@ -1,3 +1,4 @@
import MODULE_DEFINITIONS from "../../definitions"
import {
InternalModuleDeclaration,
ModuleDefinition,
@@ -5,7 +6,6 @@ import {
MODULE_SCOPE,
} from "../../types"
import { registerModules } from "../register-modules"
import MODULE_DEFINITIONS from "../../definitions"
const RESOLVED_PACKAGE = "@medusajs/test-service-resolved"
jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE))
@@ -35,7 +35,7 @@ describe("module definitions loader", () => {
it("Resolves module with default definition given empty config", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ modules: {} })
const res = registerModules({})
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
@@ -54,9 +54,7 @@ describe("module definitions loader", () => {
it("Resolves module with no resolution path when given false", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({
modules: { [defaultDefinition.key]: false },
})
const res = registerModules({ [defaultDefinition.key]: false })
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
@@ -72,9 +70,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition, isRequired: true })
try {
registerModules({
modules: { [defaultDefinition.key]: false },
})
registerModules({ [defaultDefinition.key]: false })
} catch (err) {
expect(err.message).toEqual(
`Module: ${defaultDefinition.label} is required`
@@ -90,9 +86,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push(definition)
const res = registerModules({
modules: {},
})
const res = registerModules({})
expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({
@@ -113,9 +107,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({
modules: {
[defaultDefinition.key]: defaultDefinition.defaultPackage,
},
[defaultDefinition.key]: defaultDefinition.defaultPackage,
})
expect(res[defaultDefinition.key]).toEqual(
@@ -137,13 +129,11 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({
modules: {
[defaultDefinition.key]: {
scope: MODULE_SCOPE.INTERNAL,
resolve: defaultDefinition.defaultPackage,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
} as InternalModuleDeclaration,
},
[defaultDefinition.key]: {
scope: MODULE_SCOPE.INTERNAL,
resolve: defaultDefinition.defaultPackage,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
} as InternalModuleDeclaration,
})
expect(res[defaultDefinition.key]).toEqual(
@@ -164,10 +154,8 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({
modules: {
[defaultDefinition.key]: {
options: { test: 123 },
},
[defaultDefinition.key]: {
options: { test: 123 },
},
} as any)
@@ -189,13 +177,11 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({
modules: {
[defaultDefinition.key]: {
resolve: defaultDefinition.defaultPackage,
options: { test: 123 },
scope: "internal",
resources: "isolated",
},
[defaultDefinition.key]: {
resolve: defaultDefinition.defaultPackage,
options: { test: 123 },
scope: "internal",
resources: "isolated",
},
} as any)

View File

@@ -1,17 +1,22 @@
import resolveCwd from "resolve-cwd"
import MODULE_DEFINITIONS from "../definitions"
import {
MedusaModuleConfig,
ExternalModuleDeclaration,
InternalModuleDeclaration,
ModuleDefinition,
ModuleResolution,
MODULE_SCOPE,
} from "../types"
import MODULE_DEFINITIONS from "../definitions"
export const registerModules = ({
modules,
}: MedusaModuleConfig): Record<string, ModuleResolution> => {
export const registerModules = (
modules?: Record<
string,
| false
| string
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
>
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
const projectModules = modules ?? {}
@@ -26,10 +31,32 @@ export const registerModules = ({
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
projectModules[definition.key] as
| InternalModuleDeclaration
| false
| string
customConfig as InternalModuleDeclaration
)
}
return moduleResolutions
}
export const registerMedusaModule = (
moduleKey: string,
moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
for (const definition of MODULE_DEFINITIONS) {
if (definition.key !== moduleKey) {
continue
}
if (moduleDeclaration.scope === MODULE_SCOPE.EXTERNAL) {
// TODO: getExternalModuleResolution(...)a
throw new Error("External Modules are not supported yet.")
}
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
moduleDeclaration as InternalModuleDeclaration
)
}

View File

@@ -36,7 +36,7 @@ export async function loadInternalModule(
return {
error: new Error(
"No service found in module. Make sure your module exports at least one service."
"No service found in module. Make sure your module exports a service."
),
}
}
@@ -112,3 +112,15 @@ export async function loadInternalModule(
"module"
)
}
export async function loadModuleMigrations(
resolution: ModuleResolution
): Promise<[Function | undefined, Function | undefined]> {
let loadedModule: ModuleExports
try {
loadedModule = (await import(resolution.resolutionPath as string)).default
return [loadedModule.runMigrations, loadedModule.revertMigration]
} catch {
return [undefined, undefined]
}
}

View File

@@ -0,0 +1,108 @@
import { asValue } from "awilix"
import { createMedusaContainer } from "medusa-core-utils"
import { moduleLoader, registerMedusaModule } from "./loaders"
import { loadModuleMigrations } from "./loaders/utils"
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
} from "./types"
const logger: any = {
log: (a) => console.log(a),
info: (a) => console.log(a),
warn: (a) => console.warn(a),
error: (a) => console.error(a),
}
export class MedusaModule {
public static async bootstrap(
moduleKey: string,
defaultPath: string,
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
injectedDependencies?: Record<string, any>
): Promise<{
[key: string]: any
}> {
let modDeclaration = declaration
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
modDeclaration = {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: defaultPath,
options: declaration,
}
}
const container = createMedusaContainer()
if (injectedDependencies) {
for (const service in injectedDependencies) {
container.register(service, asValue(injectedDependencies[service]))
}
}
const moduleResolutions = registerMedusaModule(moduleKey, modDeclaration!)
await moduleLoader({ container, moduleResolutions, logger })
const services = {}
for (const resolution of Object.values(moduleResolutions)) {
const registrationName = resolution.definition.registrationName
services[registrationName] = container.resolve(registrationName)
}
return services
}
public static async migrateUp(
moduleKey: string,
modulePath: string,
options?: Record<string, any>
): Promise<void> {
const moduleResolutions = registerMedusaModule(moduleKey, {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: modulePath,
options,
})
for (const mod in moduleResolutions) {
const [migrateUp] = await loadModuleMigrations(moduleResolutions[mod])
if (typeof migrateUp === "function") {
await migrateUp({
options,
logger,
})
}
}
}
public static async migrateDown(
moduleKey: string,
modulePath: string,
options?: Record<string, any>
): Promise<void> {
const moduleResolutions = registerMedusaModule(moduleKey, {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: modulePath,
options,
})
for (const mod in moduleResolutions) {
const [, migrateDown] = await loadModuleMigrations(moduleResolutions[mod])
if (typeof migrateDown === "function") {
await migrateDown({
options,
logger,
})
}
}
}
}

View File

@@ -68,9 +68,9 @@ export type ModuleDefinition = {
| ExternalModuleDeclaration
}
export type LoaderOptions = {
export type LoaderOptions<TOptions = Record<string, unknown>> = {
container: MedusaContainer
options?: Record<string, unknown>
options?: TOptions
logger?: Logger
}
@@ -89,13 +89,12 @@ export type ModuleExports = {
loaders?: ModuleLoaderFunction[]
migrations?: any[]
models?: Constructor<any>[]
}
export type MedusaModuleConfig = {
modules?: Record<
string,
| false
| string
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
>
runMigrations?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
revertMigration?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
}