cdimascio / express-openapi-validator

🦋 Auto-validates api requests, responses, and securities using ExpressJS and an OpenAPI 3.x specification
MIT License
920 stars 211 forks source link

Support ESM import of handler module in default resolver #982

Open cvchauhan opened 2 months ago

cvchauhan commented 2 months ago

Make the default request handler resolver work in ESM projects by allowing a URL value for options.operationHandlers. If a URL is passed, then a dynamic import will be used to load handler modules. If a string is passed, then require() will be used.

Existing projects should not be adversely affected by this change. The assignment of a URL value to options.operationHandlers would be the indicator that a user wants to opt-in to this new import handling.

Fixes https://github.com/cdimascio/express-openapi-validator/issues/660 Fixes https://github.com/cdimascio/express-openapi-validator/issues/838

Notice Because this project uses moduleResolution:node in tsconfig.json, it's not possible to write async import(...) directly in code. It has to be obscured so that the compiler does not replace it with require(). Hence, there is a very obvious HACK! in this PR that obscures import() via Function('x', 'return import(x)') (it's very similar to eval('import(...)')).

This hack will probably sink this PR, but I figured I'd post it at least as a proof of concept. Ideally, the module resolution for tsconfig would be updated to Node16, but that brings its own issues since resolveJsonModule:true would no longer work. Fix one issue, introduce another...

import {fileURLToPath, pathToFileURL} from 'node:url';
import path from 'node:path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const handlersPath = path.join(__dirname, '../path/to/my-routes');

const eov = OpenApiValidator.middleware({
  operationHandlers: pathToFileURL(handlersPath),
  // <rest of configuration>
});
app.use(eov);

When defining x-eov-operation-handler in schemas, include the file extension on the module name. For example:

{
  "get": {
    "summary": "Get thing",
    "x-eov-operation-id": "getThing",
    "x-eov-operation-handler": "things.js", // <-- include file extension
    "responses": {