diff --git a/src/framework/types.ts b/src/framework/types.ts index 9d4d0c65..7004eb6a 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -63,9 +63,9 @@ export type ValidateSecurityOpts = { }; export type OperationHandlerOptions = { - basePath: string; + basePath: string | URL; resolver: ( - handlersPath: string, + handlersPath: string | URL, route: RouteMetadata, apiDoc: OpenAPIV3.Document, ) => RequestHandler | Promise; @@ -155,7 +155,7 @@ export interface OpenApiValidatorOpts { $refParser?: { mode: 'bundle' | 'dereference'; }; - operationHandlers?: false | string | OperationHandlerOptions; + operationHandlers?: false | string | URL | OperationHandlerOptions; validateFormats?: boolean | 'fast' | 'full'; } diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index 3265c187..3d130a35 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -56,7 +56,7 @@ export class OpenApiValidator { if (options.formats == null) options.formats = {}; if (options.useRequestUrl == null) options.useRequestUrl = false; - if (typeof options.operationHandlers === 'string') { + if (typeof options.operationHandlers === 'string' || options.operationHandlers instanceof URL) { /** * Internally, we want to convert this to a value typed OperationHandlerOptions. * In this way, we can treat the value as such when we go to install (rather than diff --git a/src/resolvers.ts b/src/resolvers.ts index 3092e3a3..dc0d8851 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -2,40 +2,47 @@ import * as path from 'path'; import { RequestHandler } from 'express'; import { RouteMetadata } from './framework/openapi.spec.loader'; import { OpenAPIV3 } from './framework/types'; +import { fileURLToPath, pathToFileURL } from 'url'; + +// Prevent TypeScript from replacing dynamic import with require() +const dynamicImport = new Function('specifier', 'return import(specifier)'); const cache = {}; -export function defaultResolver( - handlersPath: string, +export async function defaultResolver( + handlersPath: string | URL, route: RouteMetadata, apiDoc: OpenAPIV3.Document, -): RequestHandler { - const tmpModules = {}; +): Promise { const { basePath, expressRoute, openApiRoute, method } = route; const pathKey = openApiRoute.substring(basePath.length); const schema = apiDoc.paths[pathKey][method.toLowerCase()]; const oId = schema['x-eov-operation-id'] || schema['operationId']; - const baseName = schema['x-eov-operation-handler']; + const handlerName = schema['x-eov-operation-handler']; - const cacheKey = `${expressRoute}-${method}-${oId}-${baseName}`; + const cacheKey = `${expressRoute}-${method}-${oId}-${handlerName}`; if (cache[cacheKey]) return cache[cacheKey]; - if (oId && !baseName) { + if (oId && !handlerName) { throw Error( `found x-eov-operation-id for route ${method} - ${expressRoute}]. x-eov-operation-handler required.`, ); } - if (!oId && baseName) { + if (!oId && handlerName) { throw Error( `found x-eov-operation-handler for route [${method} - ${expressRoute}]. operationId or x-eov-operation-id required.`, ); } - if (oId && baseName && typeof handlersPath === 'string') { - const modulePath = path.join(handlersPath, baseName); - if (!tmpModules[modulePath]) { - tmpModules[modulePath] = require(modulePath); - } - const handler = tmpModules[modulePath][oId] || tmpModules[modulePath].default[oId] || tmpModules[modulePath].default; + const isHandlerPath = !!handlersPath && (typeof handlersPath === 'string' || handlersPath instanceof URL); + if (oId && handlerName && isHandlerPath) { + const modulePath = typeof handlersPath === 'string' + ? path.join(handlersPath, handlerName) + : path.join(fileURLToPath(handlersPath), handlerName); + const importedModule = typeof handlersPath === 'string' + ? require(modulePath) + : await dynamicImport(pathToFileURL(modulePath).toString()); + + const handler = importedModule[oId] || importedModule.default?.[oId] || importedModule.default; if (!handler) { throw Error(