feat(modules-sdk): Module provider plugin loader (#6286)

This commit is contained in:
Oli Juhl
2024-02-01 17:03:31 +01:00
committed by GitHub
parent 45134e4d11
commit 3a103f0c36
13 changed files with 255 additions and 6 deletions

View File

@@ -32,6 +32,7 @@ export default async ({
options?.providers?.map((provider) => [provider.name, provider.scopes]) ??
[]
)
// if(options?.providers?.length) {
// TODO: implement plugin provider registration
// }

View File

@@ -0,0 +1,5 @@
const service = class TestService {}
export default {
services: [service],
}

View File

@@ -0,0 +1,5 @@
const service = class TestService {}
export const defaultExport = {
services: [service],
}

View File

@@ -0,0 +1,3 @@
export default {
loaders: [],
}

View File

@@ -0,0 +1,98 @@
import { createMedusaContainer } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix"
import { moduleProviderLoader } from "../module-provider-loader"
const logger = {
warn: jest.fn(),
error: jest.fn(),
} as any
describe("modules loader", () => {
let container
afterEach(() => {
jest.clearAllMocks()
})
beforeEach(() => {
container = createMedusaContainer()
})
it("should register the provider service", async () => {
const moduleProviders = [
{
resolve: "@plugins/default",
options: {},
},
]
await moduleProviderLoader({ container, providers: moduleProviders })
const testService = container.resolve("testService")
expect(testService).toBeTruthy()
expect(testService.constructor.name).toEqual("TestService")
})
it("should register the provider service with custom register fn", async () => {
const fn = async (klass, container, details) => {
container.register({
[`testServiceCustomRegistration`]: asFunction(
(cradle) => new klass(cradle, details.options),
{
lifetime: Lifetime.SINGLETON,
}
),
})
}
const moduleProviders = [
{
resolve: "@plugins/default",
options: {},
},
]
await moduleProviderLoader({
container,
providers: moduleProviders,
registerServiceFn: fn,
})
const testService = container.resolve("testServiceCustomRegistration")
expect(testService).toBeTruthy()
expect(testService.constructor.name).toEqual("TestService")
})
it("should log the errors if no service is defined", async () => {
const moduleProviders = [
{
resolve: "@plugins/no-service",
options: {},
},
]
try {
await moduleProviderLoader({ container, providers: moduleProviders })
} catch (error) {
expect(error.message).toBe(
"No services found in plugin @plugins/no-service -- make sure your plugin has a default export of services."
)
}
})
it("should throw if no default export is defined", async () => {
const moduleProviders = [
{
resolve: "@plugins/no-default",
options: {},
},
]
try {
await moduleProviderLoader({ container, providers: moduleProviders })
} catch (error) {
expect(error.message).toBe(
"No services found in plugin @plugins/no-default -- make sure your plugin has a default export of services."
)
}
})
})

View File

@@ -1,2 +1,4 @@
export * from "./module-loader"
export * from "./module-provider-loader"
export * from "./register-modules"

View File

@@ -0,0 +1,80 @@
import { MedusaContainer, ModuleProvider } from "@medusajs/types"
import { isString, lowerCaseFirst, promiseAll } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix"
export async function moduleProviderLoader({
container,
providers,
registerServiceFn,
}: {
container: MedusaContainer
providers: ModuleProvider[]
registerServiceFn?: (
klass,
container: MedusaContainer,
pluginDetails: any
) => Promise<void>
}) {
if (!providers?.length) {
return
}
await promiseAll(
providers.map(async (pluginDetails) => {
await loadModuleProvider(container, pluginDetails, registerServiceFn)
})
)
}
export async function loadModuleProvider(
container: MedusaContainer,
provider: ModuleProvider,
registerServiceFn?: (klass, container, pluginDetails) => Promise<void>
) {
let loadedProvider: any
const pluginName = provider.resolve ?? provider.provider_name ?? ""
try {
loadedProvider = provider.resolve
if (isString(provider.resolve)) {
loadedProvider = await import(provider.resolve)
}
} catch (error) {
throw new Error(
`Unable to find plugin ${pluginName} -- perhaps you need to install its package?`
)
}
loadedProvider = (loadedProvider as any).default ?? loadedProvider
if (!loadedProvider?.services?.length) {
throw new Error(
`No services found in plugin ${provider.resolve} -- make sure your plugin has a default export of services.`
)
}
const services = await promiseAll(
loadedProvider.services.map(async (service) => {
const name = lowerCaseFirst(service.name)
if (registerServiceFn) {
// Used to register the specific type of service in the provider
await registerServiceFn(service, container, provider.options)
} else {
container.register({
[name]: asFunction(
(cradle) => new service(cradle, provider.options),
{
lifetime: service.LIFE_TIME || Lifetime.SCOPED,
}
),
})
}
return service
})
)
return services
}

View File

@@ -1,12 +1,12 @@
import { IPaymentModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { createPaymentCollections } from "../../../__fixtures__/payment-collection"
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
import { Modules } from "@medusajs/modules-sdk"
import { initModules } from "medusa-test-utils"
import { initialize } from "../../../../src/initialize"
import { createPaymentCollections } from "../../../__fixtures__/payment-collection"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
jest.setTimeout(30000)

View File

@@ -1,2 +1,4 @@
export * from "./connection"
export * from "./container"
export * from "./providers"

View File

@@ -0,0 +1,41 @@
import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { Lifetime, asFunction } from "awilix"
const registrationFn = async (klass, container, pluginOptions) => {
container.register({
[`payment_provider_${klass.prototype}`]: asFunction(
(cradle) => new klass(cradle, pluginOptions),
{
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
}
),
})
container.registerAdd(
"payment_providers",
asFunction((cradle) => new klass(cradle, pluginOptions), {
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
})
)
}
export default async ({
container,
options,
}: LoaderOptions<
(
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
) & { providers: ModuleProvider[] }
>): Promise<void> => {
const pluginProviders =
options?.providers?.filter((provider) => provider.resolve) || []
await moduleProviderLoader({
container,
providers: pluginProviders,
registerServiceFn: registrationFn,
})
}

View File

@@ -4,6 +4,7 @@ import { PaymentModuleService } from "@services"
import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container"
import loadProviders from "./loaders/providers"
import { Modules } from "@medusajs/modules-sdk"
import { ModulesSdkUtils } from "@medusajs/utils"
@@ -24,7 +25,7 @@ export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript(
)
const service = PaymentModuleService
const loaders = [loadContainer, loadConnection] as any
const loaders = [loadContainer, loadConnection, loadProviders] as any
export const moduleDefinition: ModuleExports = {
service,

View File

@@ -290,3 +290,13 @@ export interface IModuleService {
onApplicationStart?: () => Promise<void>
}
}
export type ModuleProviderExports = {
services: Constructor<any>[]
}
export type ModuleProvider = {
resolve: string | ModuleProviderExports
provider_name?: string
options: Record<string, unknown>
}

View File

@@ -41,3 +41,4 @@ export * from "./to-pascal-case"
export * from "./transaction"
export * from "./upper-case-first"
export * from "./wrap-handler"