From 7001d21129c99e70093b89f363ca4f83b4fc65bd Mon Sep 17 00:00:00 2001 From: Viljami Kuosmanen Date: Tue, 1 Jul 2025 13:51:41 +0300 Subject: [PATCH] Add lifecycle handlers preRoutingHandler postRoutingHandler postSecurityHandler preOperationHandler --- src/backend.test.ts | 117 ++++++++++++++++++++++++++++++++++++++++++++ src/backend.ts | 40 +++++++++++++-- 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/src/backend.test.ts b/src/backend.test.ts index f029134a..17fefab3 100644 --- a/src/backend.test.ts +++ b/src/backend.test.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { OpenAPIBackend, Context } from './backend'; +import type { Request } from './router'; import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; const testsDir = path.join(__dirname, '..', '__tests__'); @@ -768,4 +769,120 @@ describe('OpenAPIBackend', () => { expect(mock).toMatchObject(exampleGarfield); }); }); + + describe('lifecycle handlers', () => { + const definition = { + openapi: '3.1.0', + info: { + title: 'api', + version: '1.0.0', + }, + paths: { + '/pets': { + get: { + operationId: 'getPets', + responses: { + 200: { description: 'ok' }, + }, + }, + }, + }, + security: [ + { + basicAuth: [], + }, + ], + components: { + securitySchemes: { + basicAuth: { + type: 'http', + scheme: 'basic', + }, + }, + }, + } as OpenAPIV3_1.Document; + + let api: OpenAPIBackend; + let request: Request; + let resultOrder: string[]; + + beforeEach(async () => { + api = new OpenAPIBackend({ + definition, + }); + + resultOrder = []; + request = { + method: 'get', + path: '/pets', + headers: {}, + }; + + const addToResultOrder = (name: string) => jest.fn(async () => resultOrder.push(name)); + + api.register('getPets', addToResultOrder('operationHandler')); + api.register('notFound', addToResultOrder('notFoundHandler')); + api.register('methodNotAllowed', addToResultOrder('methodNotAllowedHandler')); + api.register('unauthorizedHandler', addToResultOrder('unauthorizedHandler')); + api.register('preRoutingHandler', addToResultOrder('preRoutingHandler')); + api.register('postRoutingHandler', addToResultOrder('postRoutingHandler')); + api.register('postSecurityHandler', addToResultOrder('postSecurityHandler')); + api.register('preOperationHandler', addToResultOrder('preOperationHandler')); + api.register('postResponseHandler', addToResultOrder('postResponseHandler')); + api.registerSecurityHandler('basicAuth', () => true); + + await api.init(); + }); + + test('should execute handlers in the correct order for a valid request', async () => { + await api.handleRequest(request); + + expect(resultOrder).toEqual([ + 'preRoutingHandler', + 'postRoutingHandler', + 'postSecurityHandler', + 'preOperationHandler', + 'operationHandler', + 'postResponseHandler', + ]); + }); + + test('should execute handlers in the correct order when route is not found', async () => { + request.path = '/unknown'; + await api.handleRequest(request); + + expect(resultOrder).toEqual([ + 'preRoutingHandler', + 'postRoutingHandler', + 'notFoundHandler', + 'postResponseHandler', + ]); + }); + + test('should execute handlers in the correct order when method is not allowed', async () => { + request.method = 'post'; // No POST handler defined in this path + await api.handleRequest(request); + + expect(resultOrder).toEqual([ + 'preRoutingHandler', + 'postRoutingHandler', + 'methodNotAllowedHandler', + 'postResponseHandler', + ]); + }); + + test('should execute handlers in the correct order when unauthorized', async () => { + api.registerSecurityHandler('basicAuth', () => false); + + await api.handleRequest(request); + + expect(resultOrder).toEqual([ + 'preRoutingHandler', + 'postRoutingHandler', + 'postSecurityHandler', + 'unauthorizedHandler', + 'postResponseHandler', + ]); + }); + }); }); diff --git a/src/backend.ts b/src/backend.ts index 683d4631..39437395 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -139,6 +139,10 @@ export class OpenAPIBackend { '400', 'validationFail', 'unauthorizedHandler', + 'preRoutingHandler', + 'postRoutingHandler', + 'postSecurityHandler', + 'preOperationHandler', 'postResponseHandler', ]; @@ -310,10 +314,22 @@ export class OpenAPIBackend { // parse request context.request = this.router.parseRequest(req); + // preRoutingHandler + const preRoutingHandler = this.handlers['preRoutingHandler']; + if (preRoutingHandler) { + await preRoutingHandler(context as Context, ...handlerArgs); + } + // match operation (routing) try { context.operation = this.router.matchOperation(req, true); } catch (err) { + // postRoutingHandler on routing failure + const postRoutingHandler = this.handlers['postRoutingHandler']; + if (postRoutingHandler) { + await postRoutingHandler(context as Context, ...handlerArgs); + } + let handler = this.handlers['404'] || this.handlers['notFound']; if (err instanceof Error && err.message.startsWith('405')) { // 405 method not allowed @@ -330,6 +346,12 @@ export class OpenAPIBackend { // parse request again now with matched operation context.request = this.router.parseRequest(req, context.operation); + // postRoutingHandler on routing success + const postRoutingHandler = this.handlers['postRoutingHandler']; + if (postRoutingHandler) { + await postRoutingHandler(context as Context, ...handlerArgs); + } + // get security requirements for the matched operation // global requirements are already included in the router const securityRequirements = context.operation.security || []; @@ -398,6 +420,12 @@ export class OpenAPIBackend { ...securityHandlerResults, }; + // postSecurityHandler + const postSecurityHandler = this.handlers['postSecurityHandler']; + if (postSecurityHandler) { + await postSecurityHandler(context as Context, ...handlerArgs); + } + // call unauthorizedHandler handler if auth fails if (!authorized && securityRequirements.length > 0) { const unauthorizedHandler = this.handlers['unauthorizedHandler']; @@ -430,9 +458,15 @@ export class OpenAPIBackend { } } + // preOperationHandler – runs just before the operation handler + const preOperationHandler = this.handlers['preOperationHandler']; + if (preOperationHandler) { + await preOperationHandler(context as Context, ...handlerArgs); + } + // get operation handler - const routeHandler = this.handlers[operationId]; - if (!routeHandler) { + const operationHandler = this.handlers[operationId]; + if (!operationHandler) { // 501 not implemented const notImplementedHandler = this.handlers['501'] || this.handlers['notImplemented']; if (!notImplementedHandler) { @@ -442,7 +476,7 @@ export class OpenAPIBackend { } // handle route - return routeHandler(context as Context, ...handlerArgs); + return operationHandler(context as Context, ...handlerArgs); }).bind(this)(); // post response handler