feat: Add support for providers to validate their options at loading time (#8853)

* feat: Add support for providers to validate their options at loading time

* fix missing removal

* fix integration tests

* add tests
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-08-29 09:08:49 +02:00
1 parent b8572165cb
commit 77b874f272
11 files changed
+124 -48

No files matched your search

@@ -0,0 +1,9 @@
const service = class TestService {
static validateOptions(options: Record<any, any>) {
throw new Error("Wrong options")
}
}
export default {
services: [service],
}
@@ -1,5 +1,5 @@
import { createMedusaContainer } from "@medusajs/utils" import { createMedusaContainer } from "@medusajs/utils"
import { Lifetime, asFunction } from "awilix" import { asFunction, Lifetime } from "awilix"
import { moduleProviderLoader } from "../module-provider-loader" import { moduleProviderLoader } from "../module-provider-loader"
const logger = { const logger = {
@@ -34,6 +34,24 @@ describe("modules loader", () => {
expect(testService.constructor.name).toEqual("TestService") expect(testService.constructor.name).toEqual("TestService")
}) })
it("should fail to register the provider service", async () => {
const moduleProviders = [
{
resolve: "@providers/default-with-fail-validation",
id: "default",
options: {},
},
]
const err = await moduleProviderLoader({
container,
providers: moduleProviders,
}).catch((e) => e)
expect(err).toBeTruthy()
expect(err.message).toBe("Wrong options")
})
it("should register the provider service with custom register fn", async () => { it("should register the provider service with custom register fn", async () => {
const fn = async (klass, container, details) => { const fn = async (klass, container, details) => {
container.register({ container.register({
@@ -64,6 +82,35 @@ describe("modules loader", () => {
expect(testService.constructor.name).toEqual("TestService") expect(testService.constructor.name).toEqual("TestService")
}) })
it("should fail to 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: "@providers/default-with-fail-validation",
id: "default",
options: {},
},
]
const err = await moduleProviderLoader({
container,
providers: moduleProviders,
registerServiceFn: fn,
}).catch((e) => e)
expect(err).toBeTruthy()
expect(err.message).toBe("Wrong options")
})
it("should log the errors if no service is defined", async () => { it("should log the errors if no service is defined", async () => {
const moduleProviders = [ const moduleProviders = [
{ {
@@ -60,8 +60,11 @@ export async function loadModuleProvider(
) )
} }
const services = await promiseAll( return await promiseAll(
loadedProvider.services.map(async (service) => { loadedProvider.services.map(async (service) => {
// Ask the provider to validate its options
await service.validateOptions?.(provider.options)
const name = lowerCaseFirst(service.name) const name = lowerCaseFirst(service.name)
if (registerServiceFn) { if (registerServiceFn) {
// Used to register the specific type of service in the provider // Used to register the specific type of service in the provider
@@ -83,6 +86,4 @@ export async function loadModuleProvider(
return service return service
}) })
) )
return services
} }
@@ -1,7 +1,7 @@
import { import {
AuthIdentityProviderService,
AuthenticationInput, AuthenticationInput,
AuthenticationResponse, AuthenticationResponse,
AuthIdentityProviderService,
IAuthProvider, IAuthProvider,
} from "@medusajs/types" } from "@medusajs/types"
@@ -74,6 +74,7 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
* @ignore * @ignore
*/ */
private static DISPLAY_NAME: string private static DISPLAY_NAME: string
/** /**
* @ignore * @ignore
*/ */
@@ -91,6 +92,12 @@ export abstract class AbstractAuthModuleProvider implements IAuthProvider {
return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME return (this.constructor as typeof AbstractAuthModuleProvider).DISPLAY_NAME
} }
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* @ignore * @ignore
* *
@@ -51,6 +51,12 @@ export class AbstractFileProviderService implements IFileProvider {
*/ */
static identifier: string static identifier: string
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* @ignore * @ignore
*/ */
@@ -11,6 +11,12 @@ export class AbstractFulfillmentProviderService
return obj?.constructor?._isFulfillmentService return obj?.constructor?._isFulfillmentService
} }
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
getIdentifier() { getIdentifier() {
return (this.constructor as any).identifier return (this.constructor as any).identifier
} }
@@ -1,4 +1,4 @@
import { NotificationTypes, INotificationProvider } from "@medusajs/types" import { INotificationProvider, NotificationTypes } from "@medusajs/types"
/** /**
* ### constructor * ### constructor
@@ -48,6 +48,12 @@ import { NotificationTypes, INotificationProvider } from "@medusajs/types"
export class AbstractNotificationProviderService export class AbstractNotificationProviderService
implements INotificationProvider implements INotificationProvider
{ {
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* This method is used to send a notification using the third-party provider or your custom logic. * This method is used to send a notification using the third-party provider or your custom logic.
* *
@@ -7,13 +7,18 @@ import {
PaymentSessionStatus, PaymentSessionStatus,
ProviderWebhookPayload, ProviderWebhookPayload,
UpdatePaymentProviderSession, UpdatePaymentProviderSession,
WebhookActionResult WebhookActionResult,
} from "@medusajs/types" } from "@medusajs/types"
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>> export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
implements IPaymentProvider implements IPaymentProvider
{ {
/**
* Override this static method in order for the loader to validate the options provided to the module provider.
* @param options
*/
static validateOptions(options: Record<any, any>): void | never {}
/** /**
* You can use the `constructor` of the provider's service to access resources in your module's container. * You can use the `constructor` of the provider's service to access resources in your module's container.
* *
@@ -1,9 +1,10 @@
import { generateJwtToken, MedusaError } from "@medusajs/utils" import { generateJwtToken, MedusaError } from "@medusajs/utils"
import { GoogleAuthService } from "../../src/services/google" import { GoogleAuthService } from "../../src/services/google"
jest.setTimeout(100000)
import { http, HttpResponse } from "msw" import { http, HttpResponse } from "msw"
import { setupServer } from "msw/node" import { setupServer } from "msw/node"
jest.setTimeout(100000)
const sampleIdPayload = { const sampleIdPayload = {
iss: "https://accounts.google.com", iss: "https://accounts.google.com",
azp: "199301612397-l1lrg08vd6dvu98r43l7ul0ri2rd2b6r.apps.googleusercontent.com", azp: "199301612397-l1lrg08vd6dvu98r43l7ul0ri2rd2b6r.apps.googleusercontent.com",
@@ -97,15 +98,10 @@ describe("Google auth provider", () => {
it("throw an error if required options are not passed", async () => { it("throw an error if required options are not passed", async () => {
let msg = "" let msg = ""
try { try {
new GoogleAuthService( GoogleAuthService.validateOptions({
{
logger: console as any,
},
{
clientID: "test", clientID: "test",
clientSecret: "test", clientSecret: "test",
} as any } as any)
)
} catch (e) { } catch (e) {
msg = e.message msg = e.message
} }
@@ -19,12 +19,25 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
protected config_: LocalServiceConfig protected config_: LocalServiceConfig
protected logger_: Logger protected logger_: Logger
static validateOptions(options: GoogleAuthProviderOptions) {
if (!options.clientID) {
throw new Error("Google clientID is required")
}
if (!options.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!options.callbackURL) {
throw new Error("Google callbackUrl is required")
}
}
constructor( constructor(
{ logger }: InjectedDependencies, { logger }: InjectedDependencies,
options: GoogleAuthProviderOptions options: GoogleAuthProviderOptions
) { ) {
super({}, { provider: "google", displayName: "Google Authentication" }) super({}, { provider: "google", displayName: "Google Authentication" })
this.validateConfig(options)
this.config_ = options this.config_ = options
this.logger_ = logger this.logger_ = logger
} }
@@ -173,18 +186,4 @@ export class GoogleAuthService extends AbstractAuthModuleProvider {
return { success: true, location: authUrl.toString() } return { success: true, location: authUrl.toString() }
} }
private validateConfig(config: LocalServiceConfig) {
if (!config.clientID) {
throw new Error("Google clientID is required")
}
if (!config.clientSecret) {
throw new Error("Google clientSecret is required")
}
if (!config.callbackURL) {
throw new Error("Google callbackUrl is required")
}
}
} }
@@ -13,12 +13,12 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { import {
AbstractPaymentProvider, AbstractPaymentProvider,
MedusaError,
PaymentActions,
PaymentSessionStatus,
isDefined, isDefined,
isPaymentProviderError, isPaymentProviderError,
isPresent, isPresent,
MedusaError,
PaymentActions,
PaymentSessionStatus,
} from "@medusajs/utils" } from "@medusajs/utils"
import { import {
ErrorCodes, ErrorCodes,
@@ -36,6 +36,12 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
protected stripe_: Stripe protected stripe_: Stripe
protected container_: MedusaContainer protected container_: MedusaContainer
static validateOptions(options: StripeOptions): void {
if (!isDefined(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin")
}
}
protected constructor(container: MedusaContainer, options: StripeOptions) { protected constructor(container: MedusaContainer, options: StripeOptions) {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
@@ -43,23 +49,11 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
this.container_ = container this.container_ = container
this.options_ = options this.options_ = options
this.stripe_ = this.init() this.stripe_ = new Stripe(options.apiKey)
}
protected init() {
this.validateOptions(this.config)
return new Stripe(this.config.apiKey)
} }
abstract get paymentIntentOptions(): PaymentIntentOptions abstract get paymentIntentOptions(): PaymentIntentOptions
private validateOptions(options: StripeOptions): void {
if (!isDefined(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin")
}
}
get options(): StripeOptions { get options(): StripeOptions {
return this.options_ return this.options_
} }
@@ -370,7 +364,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return this.stripe_.webhooks.constructEvent( return this.stripe_.webhooks.constructEvent(
data.rawData as string | Buffer, data.rawData as string | Buffer,
signature, signature,
this.config.webhookSecret this.options_.webhookSecret
) )
} }
protected buildError( protected buildError(