Skip to content

Commit

Permalink
Support ESM import of handler module in default resolver
Browse files Browse the repository at this point in the history
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": {
       ...
```
  • Loading branch information
mdmower-csnw committed Jun 2, 2024
1 parent a59f659 commit ee2841e
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 18 deletions.
6 changes: 3 additions & 3 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestHandler>;
Expand Down Expand Up @@ -155,7 +155,7 @@ export interface OpenApiValidatorOpts {
$refParser?: {
mode: 'bundle' | 'dereference';
};
operationHandlers?: false | string | OperationHandlerOptions;
operationHandlers?: false | string | URL | OperationHandlerOptions;
validateFormats?: boolean | 'fast' | 'full';
}

Expand Down
2 changes: 1 addition & 1 deletion src/openapi.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 21 additions & 14 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestHandler> {
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(
Expand Down

0 comments on commit ee2841e

Please sign in to comment.