From ee2841e0ae385477ad7afd9e764739bf4844c487 Mon Sep 17 00:00:00 2001 From: Matt Mower Date: Wed, 29 May 2024 14:25:06 -0700 Subject: [PATCH 1/2] Support ESM import of handler module in default resolver Make the default request handler resolver work in ESM projects by allowing a URL value for the handlersPath. If a URL is passed, then a dynamic import will be used to load the handler module. If a string is passed, then require() will be used. Because this project uses moduleResolution:node in tsconfig, it's not possible to write async import(...) directly in code. It has to be obscured so that the compiler does not replace it, hence the HACK! function that is essentially eval('import(...)'). ESM users should be aware that a file extension must be specified when defining x-eov-operation-handler in schemas. For example: ``` { "get": { "summary": "Get list", "x-eov-operation-id": "getList", "x-eov-operation-handler": "list.js", "responses": { ... ``` --- src/framework/types.ts | 6 +++--- src/openapi.validator.ts | 2 +- src/resolvers.ts | 35 +++++++++++++++++++++-------------- 3 files changed, 25 insertions(+), 18 deletions(-) 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( From a3eb7df4846673b3dddebd4bebebc824b0d0fd4e Mon Sep 17 00:00:00 2001 From: Matt Mower Date: Sun, 2 Jun 2024 11:37:51 -0700 Subject: [PATCH 2/2] Minor cleanup - Restore original "baseName" variable name - Make sure string path handling is exactly the same as prior to URL handling was added --- src/resolvers.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/resolvers.ts b/src/resolvers.ts index dc0d8851..3bf15152 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -17,27 +17,27 @@ export async function defaultResolver( const pathKey = openApiRoute.substring(basePath.length); const schema = apiDoc.paths[pathKey][method.toLowerCase()]; const oId = schema['x-eov-operation-id'] || schema['operationId']; - const handlerName = schema['x-eov-operation-handler']; + const baseName = schema['x-eov-operation-handler']; - const cacheKey = `${expressRoute}-${method}-${oId}-${handlerName}`; + const cacheKey = `${expressRoute}-${method}-${oId}-${baseName}`; if (cache[cacheKey]) return cache[cacheKey]; - if (oId && !handlerName) { + if (oId && !baseName) { throw Error( `found x-eov-operation-id for route ${method} - ${expressRoute}]. x-eov-operation-handler required.`, ); } - if (!oId && handlerName) { + if (!oId && baseName) { throw Error( `found x-eov-operation-handler for route [${method} - ${expressRoute}]. operationId or x-eov-operation-id required.`, ); } - const isHandlerPath = !!handlersPath && (typeof handlersPath === 'string' || handlersPath instanceof URL); - if (oId && handlerName && isHandlerPath) { + const hasHandlerPath = typeof handlersPath === 'string' || handlersPath instanceof URL; + if (oId && baseName && hasHandlerPath) { const modulePath = typeof handlersPath === 'string' - ? path.join(handlersPath, handlerName) - : path.join(fileURLToPath(handlersPath), handlerName); + ? path.join(handlersPath, baseName) + : path.join(fileURLToPath(handlersPath), baseName); const importedModule = typeof handlersPath === 'string' ? require(modulePath) : await dynamicImport(pathToFileURL(modulePath).toString());