ba-web-framework/src/decorators/Controller.ts
Alan Bridgeman b357cf3f8f
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 30s
Added more stuff for debugging to figure out what is happening
2025-06-24 13:37:18 -05:00

159 lines
No EOL
8.5 KiB
TypeScript

import { Application, Request, Response, NextFunction } from 'express';
import { BaseController } from '../controllers/BaseController';
import { CHILD_CONTROLLER_METADATA_KEY } from './ChildController';
import { GET_METADATA_KEY } from './GET';
import { POST_METADATA_KEY } from './POST';
import { PUT_METADATA_KEY } from './PUT';
import { DELETE_METADATA_KEY } from './DELETE';
/**
* Class decorator to "fill in" the setup method which is called on server startup and connects the methods/functions and their desired paths in the express app.
*
* That is, this is largely for the convenience of controller writers/developers so that they don't have to manually create a `setup` method in their controller classes.
*
* @example
* The following example show how to use the Controller decorator to setup a path `/path` with a GET, POST, PUT and DELETE method.
* ```ts
* import { Request, Response } from 'express';
*
* import { Controller, GET, POST, BaseController } from '@BridgemanAccessible/ba-web-framework';
*
* @Controller()
* export class MyController extends BaseController {
* @GET('/path')
* private myGetMethod(req: Request, res: Response) {}
*
* @POST('/path', express.json())
* private myPostMethod(req: Request, res: Response) {}
*
* @PUT('/path', express.json())
* private myPutMethod(req: Request, res: Response) {}
*
* @DELETE('/path', express.json())
* private myDeleteMethod(req: Request, res: Response) {}
* }
* ```
*/
export function Controller<T extends { new (...args: any[]): BaseController }>() {
// Technically the function we return here with the target parameter is the actual decorator itself but we wrap it in case we ever want to add parameters to the decorator
return function(target: T){
Reflect.defineMetadata('originalClass', target, target);
// We extend the class that is decorated and override the setup method to automatically setup the routes
return class extends target {
/**
* Setup the routes for the controller.
*
* @param app The express application to setup the routes on.
*/
static setup(app: Application) {
// If the decorated class is also decorated with the `@ChildController` decorator,
// then we call the child controller's setup method as well.
//
// Currently, there is very little linkage between the decorated controller class and the child controller class(es).
// Though in the future this may look more like forcing these to be on sub-paths of the parent controller etc...
const childControllers = Reflect.getMetadata(CHILD_CONTROLLER_METADATA_KEY, target);
if(typeof childControllers !== 'undefined') {
if(Array.isArray(childControllers)) {
childControllers.forEach((childController) => {
childController.setup(app);
});
} else {
childControllers.setup(app);
}
}
const controller = new (Reflect.getMetadata('originalClass', target))();
// Loop over all the methods in the decorated class looking for methods that use the GET decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have a Get metadata key (GET decorator)
.filter((method) => Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method))
.map((method) => {
// Get the method
let fn = target.prototype[method];
// Get the path
const path = Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method);
// Bind the method to the class instance
app.get(path, async (req, res, next) => {
console.log('[Controller.setup.<GET request>] Request:', req);
console.log('[Controller.setup.<GET request>] Response:', res);
console.log('[Controller.setup.<GET request>] Next:', next);
try {
return await fn.bind(controller)(req, res, next);
}
catch(error) {
console.log('[Controller.setup.<GET request>] Error:', error);
if(typeof next !== 'undefined') {
console.log('[Controller.setup.<GET request>] Calling next with error:', error);
next(error);
return;
}
else {
console.log('[Controller.setup.<GET request>] No next function defined, rejecting promise with error:', error);
// Because next is undefined and we still want to have Express handle the error we reject the promise
return Promise.reject(error);
}
}
});
});
// Loop over all the methods in the decorated class looking for methods that use the POST decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have a Post metadata key (POST decorator)
.filter((method) => Reflect.getMetadata(POST_METADATA_KEY, target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the metadata object (which contains a path and middleware)
const postRoute = Reflect.getMetadata(POST_METADATA_KEY, target.prototype, method);
const path = postRoute.path;
const middleware: NextFunction[] = postRoute.middleware;
// Bind the method to the class instance
app.post(path, ...middleware, fn.bind(controller));
});
// Loop over all the methods in the decorated class looking for methods that use the `@PUT` decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have the associated metadata key (`@PUT` decorator)
.filter((method) => Reflect.getMetadata(PUT_METADATA_KEY, target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the metadata object (which contains a path and middleware)
const putRoute = Reflect.getMetadata(PUT_METADATA_KEY, target.prototype, method);
const path = putRoute.path;
const middleware = putRoute.middleware;
// Bind the method to the class instance
app.put(path, ...middleware, fn.bind(controller));
});
// Loop over all the methods in the decorated class looking for methods that use the `@DELETE` decorator
Object.getOwnPropertyNames(target.prototype)
// Find all methods that have the associated metadata key (`@DELETE` decorator)
.filter((method) => Reflect.getMetadata(DELETE_METADATA_KEY, target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the metadata object (which contains a path and middleware)
const deleteRoute = Reflect.getMetadata(DELETE_METADATA_KEY, target.prototype, method);
const path = deleteRoute.path;
const middleware = deleteRoute.middleware;
// Bind the method to the class instance
app.delete(path, ...middleware, fn.bind(controller));
});
}
}
}
}