Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Feb 1, 2024
1 change: 1 addition & 0 deletions packages/auth/src/loaders/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default async ({
options?.providers?.map((provider) => [provider.name, provider.scopes]) ??
[]
)

// if(options?.providers?.length) {
// TODO: implement plugin provider registration
// }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const service = class TestService {}

export default {
services: [service],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const service = class TestService {}

export const defaultExport = {
services: [service],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
loaders: [],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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(typeof testService).toEqual("object")
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved
})

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(typeof testService).toEqual("object")
})

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 exports a service."
)
}
})

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 default export found in plugin @plugins/no-default -- make sure your plugin has a default export."
)
}
})
})
2 changes: 2 additions & 0 deletions packages/modules-sdk/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./module-loader"
export * from "./module-provider-loader"
export * from "./register-modules"

86 changes: 86 additions & 0 deletions packages/modules-sdk/src/loaders/module-provider-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { MedusaContainer, ModuleProvider } from "@medusajs/types"
import { isString, 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 {
if (isString(provider.resolve)) {
loadedProvider = await import(provider.resolve)
} else {
loadedProvider = provider.resolve
}
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
throw new Error(
`Unable to find plugin ${pluginName} -- perhaps you need to install its package?`
)
}

loadedProvider = (loadedProvider as any).default
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved

if (!loadedProvider) {
throw new Error(
`No default export found in plugin ${pluginName} -- make sure your plugin has a default export.`
)
}

if (!loadedProvider.services?.length) {
throw new Error(
`No services found in plugin ${provider.resolve} -- make sure your plugin exports a service.`
)
}

const services = await promiseAll(
loadedProvider.services.map(async (service) => {
const name = service.name[0].toLowerCase() + service.name.slice(1)
olivermrbl marked this conversation as resolved.
Show resolved Hide resolved
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
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { IPaymentModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"

import { Modules } from "@medusajs/modules-sdk"
import { initModules } from "medusa-test-utils"
import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { createPaymentCollections } from "../../../__fixtures__/payment-collection"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
import { initModules } from "medusa-test-utils"
import { Modules } from "@medusajs/modules-sdk"

jest.setTimeout(30000)

Expand Down
2 changes: 2 additions & 0 deletions packages/payment/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./connection"
export * from "./container"
export * from "./providers"

41 changes: 41 additions & 0 deletions packages/payment/src/loaders/providers.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
3 changes: 2 additions & 1 deletion packages/payment/src/module-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/modules-sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
1 change: 1 addition & 0 deletions packages/utils/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export * from "./to-pascal-case"
export * from "./transaction"
export * from "./upper-case-first"
export * from "./wrap-handler"

Loading