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:
1 parent
b8572165cb
commit
77b874f272
11 files changed
+124
-48
No files matched your search
+9
@@ -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(
|
||||||
|
|||||||
Reference in new issue
Block a user