Skip to content

Commit 4c80f0f

Browse files
authored
Merge pull request #786 from openapistack/feat/lifecycle-handlers
Add lifecycle handlers
2 parents 1a37bc3 + 7001d21 commit 4c80f0f

File tree

2 files changed

+154
-3
lines changed

2 files changed

+154
-3
lines changed

src/backend.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as path from 'path';
44
import { OpenAPIBackend, Context } from './backend';
5+
import type { Request } from './router';
56
import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
67

78
const testsDir = path.join(__dirname, '..', '__tests__');
@@ -768,4 +769,120 @@ describe('OpenAPIBackend', () => {
768769
expect(mock).toMatchObject(exampleGarfield);
769770
});
770771
});
772+
773+
describe('lifecycle handlers', () => {
774+
const definition = {
775+
openapi: '3.1.0',
776+
info: {
777+
title: 'api',
778+
version: '1.0.0',
779+
},
780+
paths: {
781+
'/pets': {
782+
get: {
783+
operationId: 'getPets',
784+
responses: {
785+
200: { description: 'ok' },
786+
},
787+
},
788+
},
789+
},
790+
security: [
791+
{
792+
basicAuth: [],
793+
},
794+
],
795+
components: {
796+
securitySchemes: {
797+
basicAuth: {
798+
type: 'http',
799+
scheme: 'basic',
800+
},
801+
},
802+
},
803+
} as OpenAPIV3_1.Document;
804+
805+
let api: OpenAPIBackend<OpenAPIV3_1.Document>;
806+
let request: Request;
807+
let resultOrder: string[];
808+
809+
beforeEach(async () => {
810+
api = new OpenAPIBackend({
811+
definition,
812+
});
813+
814+
resultOrder = [];
815+
request = {
816+
method: 'get',
817+
path: '/pets',
818+
headers: {},
819+
};
820+
821+
const addToResultOrder = (name: string) => jest.fn(async () => resultOrder.push(name));
822+
823+
api.register('getPets', addToResultOrder('operationHandler'));
824+
api.register('notFound', addToResultOrder('notFoundHandler'));
825+
api.register('methodNotAllowed', addToResultOrder('methodNotAllowedHandler'));
826+
api.register('unauthorizedHandler', addToResultOrder('unauthorizedHandler'));
827+
api.register('preRoutingHandler', addToResultOrder('preRoutingHandler'));
828+
api.register('postRoutingHandler', addToResultOrder('postRoutingHandler'));
829+
api.register('postSecurityHandler', addToResultOrder('postSecurityHandler'));
830+
api.register('preOperationHandler', addToResultOrder('preOperationHandler'));
831+
api.register('postResponseHandler', addToResultOrder('postResponseHandler'));
832+
api.registerSecurityHandler('basicAuth', () => true);
833+
834+
await api.init();
835+
});
836+
837+
test('should execute handlers in the correct order for a valid request', async () => {
838+
await api.handleRequest(request);
839+
840+
expect(resultOrder).toEqual([
841+
'preRoutingHandler',
842+
'postRoutingHandler',
843+
'postSecurityHandler',
844+
'preOperationHandler',
845+
'operationHandler',
846+
'postResponseHandler',
847+
]);
848+
});
849+
850+
test('should execute handlers in the correct order when route is not found', async () => {
851+
request.path = '/unknown';
852+
await api.handleRequest(request);
853+
854+
expect(resultOrder).toEqual([
855+
'preRoutingHandler',
856+
'postRoutingHandler',
857+
'notFoundHandler',
858+
'postResponseHandler',
859+
]);
860+
});
861+
862+
test('should execute handlers in the correct order when method is not allowed', async () => {
863+
request.method = 'post'; // No POST handler defined in this path
864+
await api.handleRequest(request);
865+
866+
expect(resultOrder).toEqual([
867+
'preRoutingHandler',
868+
'postRoutingHandler',
869+
'methodNotAllowedHandler',
870+
'postResponseHandler',
871+
]);
872+
});
873+
874+
test('should execute handlers in the correct order when unauthorized', async () => {
875+
api.registerSecurityHandler('basicAuth', () => false);
876+
877+
await api.handleRequest(request);
878+
879+
expect(resultOrder).toEqual([
880+
'preRoutingHandler',
881+
'postRoutingHandler',
882+
'postSecurityHandler',
883+
'unauthorizedHandler',
884+
'postResponseHandler',
885+
]);
886+
});
887+
});
771888
});

src/backend.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export class OpenAPIBackend<D extends Document = Document> {
139139
'400',
140140
'validationFail',
141141
'unauthorizedHandler',
142+
'preRoutingHandler',
143+
'postRoutingHandler',
144+
'postSecurityHandler',
145+
'preOperationHandler',
142146
'postResponseHandler',
143147
];
144148

@@ -310,10 +314,22 @@ export class OpenAPIBackend<D extends Document = Document> {
310314
// parse request
311315
context.request = this.router.parseRequest(req);
312316

317+
// preRoutingHandler
318+
const preRoutingHandler = this.handlers['preRoutingHandler'];
319+
if (preRoutingHandler) {
320+
await preRoutingHandler(context as Context<D>, ...handlerArgs);
321+
}
322+
313323
// match operation (routing)
314324
try {
315325
context.operation = this.router.matchOperation(req, true);
316326
} catch (err) {
327+
// postRoutingHandler on routing failure
328+
const postRoutingHandler = this.handlers['postRoutingHandler'];
329+
if (postRoutingHandler) {
330+
await postRoutingHandler(context as Context<D>, ...handlerArgs);
331+
}
332+
317333
let handler = this.handlers['404'] || this.handlers['notFound'];
318334
if (err instanceof Error && err.message.startsWith('405')) {
319335
// 405 method not allowed
@@ -330,6 +346,12 @@ export class OpenAPIBackend<D extends Document = Document> {
330346
// parse request again now with matched operation
331347
context.request = this.router.parseRequest(req, context.operation);
332348

349+
// postRoutingHandler on routing success
350+
const postRoutingHandler = this.handlers['postRoutingHandler'];
351+
if (postRoutingHandler) {
352+
await postRoutingHandler(context as Context<D>, ...handlerArgs);
353+
}
354+
333355
// get security requirements for the matched operation
334356
// global requirements are already included in the router
335357
const securityRequirements = context.operation.security || [];
@@ -398,6 +420,12 @@ export class OpenAPIBackend<D extends Document = Document> {
398420
...securityHandlerResults,
399421
};
400422

423+
// postSecurityHandler
424+
const postSecurityHandler = this.handlers['postSecurityHandler'];
425+
if (postSecurityHandler) {
426+
await postSecurityHandler(context as Context<D>, ...handlerArgs);
427+
}
428+
401429
// call unauthorizedHandler handler if auth fails
402430
if (!authorized && securityRequirements.length > 0) {
403431
const unauthorizedHandler = this.handlers['unauthorizedHandler'];
@@ -430,9 +458,15 @@ export class OpenAPIBackend<D extends Document = Document> {
430458
}
431459
}
432460

461+
// preOperationHandler – runs just before the operation handler
462+
const preOperationHandler = this.handlers['preOperationHandler'];
463+
if (preOperationHandler) {
464+
await preOperationHandler(context as Context<D>, ...handlerArgs);
465+
}
466+
433467
// get operation handler
434-
const routeHandler = this.handlers[operationId];
435-
if (!routeHandler) {
468+
const operationHandler = this.handlers[operationId];
469+
if (!operationHandler) {
436470
// 501 not implemented
437471
const notImplementedHandler = this.handlers['501'] || this.handlers['notImplemented'];
438472
if (!notImplementedHandler) {
@@ -442,7 +476,7 @@ export class OpenAPIBackend<D extends Document = Document> {
442476
}
443477

444478
// handle route
445-
return routeHandler(context as Context<D>, ...handlerArgs);
479+
return operationHandler(context as Context<D>, ...handlerArgs);
446480
}).bind(this)();
447481

448482
// post response handler

0 commit comments

Comments
 (0)