diff --git a/packages/auth/src/loaders/providers.ts b/packages/auth/src/loaders/providers.ts index 4f7c11048e..9cb09d9788 100644 --- a/packages/auth/src/loaders/providers.ts +++ b/packages/auth/src/loaders/providers.ts @@ -32,6 +32,7 @@ export default async ({ options?.providers?.map((provider) => [provider.name, provider.scopes]) ?? [] ) + // if(options?.providers?.length) { // TODO: implement plugin provider registration // } diff --git a/packages/modules-sdk/src/loaders/__mocks__/@plugins/default.ts b/packages/modules-sdk/src/loaders/__mocks__/@plugins/default.ts new file mode 100644 index 0000000000..ac1a9484ed --- /dev/null +++ b/packages/modules-sdk/src/loaders/__mocks__/@plugins/default.ts @@ -0,0 +1,5 @@ +const service = class TestService {} + +export default { + services: [service], +} diff --git a/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-default.ts b/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-default.ts new file mode 100644 index 0000000000..855f6e0a54 --- /dev/null +++ b/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-default.ts @@ -0,0 +1,5 @@ +const service = class TestService {} + +export const defaultExport = { + services: [service], +} diff --git a/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-service.ts b/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-service.ts new file mode 100644 index 0000000000..d0c074f41f --- /dev/null +++ b/packages/modules-sdk/src/loaders/__mocks__/@plugins/no-service.ts @@ -0,0 +1,3 @@ +export default { + loaders: [], +} diff --git a/packages/modules-sdk/src/loaders/__tests__/module-provider-loader.ts b/packages/modules-sdk/src/loaders/__tests__/module-provider-loader.ts new file mode 100644 index 0000000000..f52adc10a8 --- /dev/null +++ b/packages/modules-sdk/src/loaders/__tests__/module-provider-loader.ts @@ -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." + ) + } + }) +}) diff --git a/packages/modules-sdk/src/loaders/index.ts b/packages/modules-sdk/src/loaders/index.ts index 0ff93bba81..c6f66e5cd6 100644 --- a/packages/modules-sdk/src/loaders/index.ts +++ b/packages/modules-sdk/src/loaders/index.ts @@ -1,2 +1,4 @@ export * from "./module-loader" +export * from "./module-provider-loader" export * from "./register-modules" + diff --git a/packages/modules-sdk/src/loaders/module-provider-loader.ts b/packages/modules-sdk/src/loaders/module-provider-loader.ts new file mode 100644 index 0000000000..388122712a --- /dev/null +++ b/packages/modules-sdk/src/loaders/module-provider-loader.ts @@ -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 +}) { + 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 +) { + 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 +} diff --git a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index c29eb90882..4aa4ba6926 100644 --- a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -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) diff --git a/packages/payment/src/loaders/index.ts b/packages/payment/src/loaders/index.ts index 3614963d8c..0446db6943 100644 --- a/packages/payment/src/loaders/index.ts +++ b/packages/payment/src/loaders/index.ts @@ -1,2 +1,4 @@ export * from "./connection" export * from "./container" +export * from "./providers" + diff --git a/packages/payment/src/loaders/providers.ts b/packages/payment/src/loaders/providers.ts new file mode 100644 index 0000000000..f2d5004697 --- /dev/null +++ b/packages/payment/src/loaders/providers.ts @@ -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 => { + const pluginProviders = + options?.providers?.filter((provider) => provider.resolve) || [] + + await moduleProviderLoader({ + container, + providers: pluginProviders, + registerServiceFn: registrationFn, + }) +} diff --git a/packages/payment/src/module-definition.ts b/packages/payment/src/module-definition.ts index 3b7c8e329a..daae25a16c 100644 --- a/packages/payment/src/module-definition.ts +++ b/packages/payment/src/module-definition.ts @@ -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, diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 5a7188cc23..c69a3e90a1 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -290,3 +290,13 @@ export interface IModuleService { onApplicationStart?: () => Promise } } + +export type ModuleProviderExports = { + services: Constructor[] +} + +export type ModuleProvider = { + resolve: string | ModuleProviderExports + provider_name?: string + options: Record +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 1c0dd744d4..7dc7fd9831 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -41,3 +41,4 @@ export * from "./to-pascal-case" export * from "./transaction" export * from "./upper-case-first" export * from "./wrap-handler" +