Attempting to fix issue with @Page decorator that didn't preserve contex + some work to make the @Controller more useful etc...
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 37s
All checks were successful
Publish to Private NPM Registry / publish (push) Successful in 37s
This commit is contained in:
parent
6fbdca6603
commit
3c44ba665e
6 changed files with 287 additions and 203 deletions
|
|
@ -1,12 +1,12 @@
|
||||||
import type { Application, Request, Response, NextFunction } from 'express';
|
import type { Application, Request, Response, NextFunction, RequestHandler } from 'express';
|
||||||
|
|
||||||
import { BaseController } from '../controllers/BaseController.js';
|
import { BaseController } from '../controllers/BaseController.js';
|
||||||
|
|
||||||
import { CHILD_CONTROLLER_METADATA_KEY } from './ChildController.js';
|
import { CHILD_CONTROLLER_METADATA_KEY } from './ChildController.js';
|
||||||
import { GET_METADATA_KEY } from './GET.js';
|
import { GET_METADATA_KEY, GET_FORCE_PREFIX_METADATA_KEY } from './GET.js';
|
||||||
import { POST_METADATA_KEY } from './POST.js';
|
import { POST_METADATA_KEY, POST_FORCE_PREFIX_METADATA_KEY } from './POST.js';
|
||||||
import { PUT_METADATA_KEY } from './PUT.js';
|
import { PUT_METADATA_KEY, PUT_FORCE_PREFIX_METADATA_KEY } from './PUT.js';
|
||||||
import { DELETE_METADATA_KEY } from './DELETE.js';
|
import { DELETE_METADATA_KEY, DELETE_FORCE_PREFIX_METADATA_KEY } from './DELETE.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* 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.
|
||||||
|
|
@ -35,19 +35,74 @@ import { DELETE_METADATA_KEY } from './DELETE.js';
|
||||||
* private myDeleteMethod(req: Request, res: Response) {}
|
* private myDeleteMethod(req: Request, res: Response) {}
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* The following example shows how to use the Controller decorator with a base path, middleware and a child controller.
|
||||||
|
* ```ts
|
||||||
|
* import { Request, Response } from 'express';
|
||||||
|
*
|
||||||
|
* import { Controller, ChildController, GET, BaseController } from '@BridgemanAccessible/ba-web-framework';
|
||||||
|
*
|
||||||
|
* @Controller('/api', myMiddlewareFunction)
|
||||||
|
* @ChildController(APIV1Controller)
|
||||||
|
* export class ApiController extends BaseController {
|
||||||
|
* @GET('/status')
|
||||||
|
* private getStatus(req: Request, res: Response) { /* ... * / }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Controller('/v1')
|
||||||
|
* export class APIV1Controller extends BaseController {
|
||||||
|
* // Full path will be /api/v1/route because of the parent controller and this controller's base paths
|
||||||
|
* @GET('/route')
|
||||||
|
* private getV1Route(req: Request, res: Response) { /* ... * / }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param basePath The base path for the controller. Defaults to `'/'`.
|
||||||
|
* @param middlewares Optional middlewares to apply to all routes in the controller.
|
||||||
*/
|
*/
|
||||||
export function Controller<T extends { new (...args: any[]): BaseController }>() {
|
export function Controller<T extends { new (...args: any[]): BaseController }>(basePath: string = '/', ...middlewares: RequestHandler[]) {
|
||||||
// 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
|
// 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){
|
return function(target: T){
|
||||||
|
Reflect.defineMetadata('basePath', basePath, target);
|
||||||
|
Reflect.defineMetadata('middlewares', middlewares, target);
|
||||||
Reflect.defineMetadata('originalClass', target, target);
|
Reflect.defineMetadata('originalClass', target, target);
|
||||||
|
|
||||||
// We extend the class that is decorated and override the setup method to automatically setup the routes
|
// We extend the class that is decorated and override the setup method to automatically setup the routes
|
||||||
return class extends target {
|
return class extends target {
|
||||||
|
// Helper function to join paths cleanly
|
||||||
|
static joinPaths(base: string, path: string): string {
|
||||||
|
// Combine the base path and the specific route path
|
||||||
|
let fullPath = base + path;
|
||||||
|
|
||||||
|
// Remove double slashes (e.g., //user -> /user)
|
||||||
|
fullPath = fullPath.replace(/\/\//g, '/');
|
||||||
|
|
||||||
|
// Ensure it starts with / (in case base was empty and path was empty)
|
||||||
|
if (!fullPath.startsWith('/')) {
|
||||||
|
fullPath = '/' + fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash (optional, unless it's just root '/')
|
||||||
|
if (fullPath.length > 1 && fullPath.endsWith('/')) {
|
||||||
|
fullPath = fullPath.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the routes for the controller.
|
* Setup the routes for the controller.
|
||||||
*
|
*
|
||||||
* @param app The express application to setup the routes on.
|
* @param app The express application to setup the routes on.
|
||||||
*/
|
*/
|
||||||
static setup(app: Application) {
|
static setup(app: Application, pathPrefix: string = '') {
|
||||||
|
// Calculate the current context's absolute prefix
|
||||||
|
// Order matters: Incoming Prefix + Current Controller Base Path
|
||||||
|
// Ex: "" + "/api" = "/api"
|
||||||
|
// Ex: "/api" + "/v1" = "/api/v1"
|
||||||
|
const currentPrefix = this.joinPaths(pathPrefix, Reflect.getMetadata('basePath', target) as string);
|
||||||
|
|
||||||
// If the decorated class is also decorated with the `@ChildController` decorator,
|
// If the decorated class is also decorated with the `@ChildController` decorator,
|
||||||
// then we call the child controller's setup method as well.
|
// then we call the child controller's setup method as well.
|
||||||
//
|
//
|
||||||
|
|
@ -57,10 +112,11 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
|
||||||
if(typeof childControllers !== 'undefined') {
|
if(typeof childControllers !== 'undefined') {
|
||||||
if(Array.isArray(childControllers)) {
|
if(Array.isArray(childControllers)) {
|
||||||
childControllers.forEach((childController) => {
|
childControllers.forEach((childController) => {
|
||||||
childController.setup(app);
|
childController.setup(app, currentPrefix);
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
childControllers.setup(app);
|
else {
|
||||||
|
childControllers.setup(app, currentPrefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,39 +133,54 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
|
||||||
// Get the path
|
// Get the path
|
||||||
const path = Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method);
|
const path = Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method);
|
||||||
|
|
||||||
|
const fullPath = Reflect.getMetadata(GET_FORCE_PREFIX_METADATA_KEY, target.prototype, method) ? this.joinPaths(currentPrefix, path) : path;
|
||||||
|
|
||||||
// Bind the method to the class instance
|
// Bind the method to the class instance
|
||||||
app.get(path, async (req, res, next) => {
|
app.get(fullPath, ...[
|
||||||
//console.log('[Controller.setup.<GET request>] Request:', req);
|
// Middleware defined at the controller level
|
||||||
//console.log('[Controller.setup.<GET request>] Response:', res);
|
...(Reflect.getMetadata('middlewares', target) as RequestHandler[]),
|
||||||
//console.log('[Controller.setup.<GET request>] Next:', next);
|
|
||||||
|
|
||||||
try {
|
// Route handler
|
||||||
await Promise.all([fn.bind(controller)(req, res, next)])
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
.catch((error) => {
|
//console.log('[Controller.setup.<GET request>] Request:', req);
|
||||||
//console.log('[Controller.setup.<GET request>] Error in promise:', error);
|
//console.log('[Controller.setup.<GET request>] Response:', res);
|
||||||
throw new Error(error);
|
//console.log('[Controller.setup.<GET request>] Next:', next);
|
||||||
});
|
|
||||||
|
|
||||||
//console.log('[Controller.setup.<GET request>] Successfully executed GET method:', method);
|
try {
|
||||||
}
|
await Promise.all([
|
||||||
catch(error) {
|
fn.bind(controller)(req, res, next)
|
||||||
//console.log('[Controller.setup.<GET request>] Error:', error);
|
])
|
||||||
if(typeof next !== 'undefined') {
|
.catch((error) => {
|
||||||
//console.log('[Controller.setup.<GET request>] Calling next with error:', error);
|
//console.log('[Controller.setup.<GET request>] Error in promise:', error);
|
||||||
|
|
||||||
next(error);
|
if(error instanceof Error) {
|
||||||
return;
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log('[Controller.setup.<GET request>] Successfully executed GET method:', method);
|
||||||
}
|
}
|
||||||
else {
|
catch(error) {
|
||||||
//console.log('[Controller.setup.<GET request>] No next function defined, rejecting promise with error:', error);
|
//console.log('[Controller.setup.<GET request>] Error:', error);
|
||||||
|
if(typeof next !== 'undefined') {
|
||||||
|
//console.log('[Controller.setup.<GET request>] Calling next with error:', error);
|
||||||
|
|
||||||
// Because next is undefined and we still want to have Express handle the error we reject the promise
|
next(error);
|
||||||
return Promise.reject(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//console.log('[Controller.setup.<GET request>] Finished processing GET request for method:', method);
|
//console.log('[Controller.setup.<GET request>] Finished processing GET request for method:', method);
|
||||||
});
|
}
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loop over all the methods in the decorated class looking for methods that use the POST decorator
|
// Loop over all the methods in the decorated class looking for methods that use the POST decorator
|
||||||
|
|
@ -125,32 +196,43 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
|
||||||
const path = postRoute.path;
|
const path = postRoute.path;
|
||||||
const middleware: NextFunction[] = postRoute.middleware;
|
const middleware: NextFunction[] = postRoute.middleware;
|
||||||
|
|
||||||
|
const fullPath = Reflect.getMetadata(POST_FORCE_PREFIX_METADATA_KEY, target.prototype, method) ? this.joinPaths(currentPrefix, path) : path;
|
||||||
|
|
||||||
// Bind the method to the class instance
|
// Bind the method to the class instance
|
||||||
app.post(path, ...middleware, async (req, res, next) => {
|
app.post(fullPath, ...[
|
||||||
//console.log('[Controller.setup.<POST request>] Request:', req);
|
// Middleware defined at the controller level
|
||||||
//console.log('[Controller.setup.<POST request>] Response:', res);
|
...(Reflect.getMetadata('middlewares', target) as RequestHandler[]),
|
||||||
//console.log('[Controller.setup.<POST request>] Next:', next);
|
|
||||||
|
|
||||||
try {
|
// Middleware defined at the route level
|
||||||
await Promise.all([fn.bind(controller)(req, res, next)]);
|
...middleware,
|
||||||
//console.log('[Controller.setup.<POST request>] Successfully executed POST method:', method);
|
|
||||||
}
|
|
||||||
catch(error) {
|
|
||||||
//console.log('[Controller.setup.<POST request>] Error:', error);
|
|
||||||
if(typeof next !== 'undefined') {
|
|
||||||
//console.log('[Controller.setup.<POST request>] Calling next with error:', error);
|
|
||||||
|
|
||||||
next(error);
|
// Route handler
|
||||||
return;
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
//console.log('[Controller.setup.<POST request>] Request:', req);
|
||||||
|
//console.log('[Controller.setup.<POST request>] Response:', res);
|
||||||
|
//console.log('[Controller.setup.<POST request>] Next:', next);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([fn.bind(controller)(req, res, next)]);
|
||||||
|
//console.log('[Controller.setup.<POST request>] Successfully executed POST method:', method);
|
||||||
}
|
}
|
||||||
else {
|
catch(error) {
|
||||||
//console.log('[Controller.setup.<POST request>] No next function defined, rejecting promise with error:', error);
|
//console.log('[Controller.setup.<POST request>] Error:', error);
|
||||||
|
if(typeof next !== 'undefined') {
|
||||||
|
//console.log('[Controller.setup.<POST request>] Calling next with error:', error);
|
||||||
|
|
||||||
// Because next is undefined and we still want to have Express handle the error we reject the promise
|
next(error);
|
||||||
return Promise.reject(error);
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//console.log('[Controller.setup.<POST 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 `@PUT` decorator
|
// Loop over all the methods in the decorated class looking for methods that use the `@PUT` decorator
|
||||||
|
|
@ -166,32 +248,38 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
|
||||||
const path = putRoute.path;
|
const path = putRoute.path;
|
||||||
const middleware = putRoute.middleware;
|
const middleware = putRoute.middleware;
|
||||||
|
|
||||||
|
const fullPath = Reflect.getMetadata(PUT_FORCE_PREFIX_METADATA_KEY, target.prototype, method) ? this.joinPaths(currentPrefix, path) : path;
|
||||||
|
|
||||||
// Bind the method to the class instance
|
// Bind the method to the class instance
|
||||||
app.put(path, ...middleware, async (req, res, next) => {
|
app.put(fullPath, ...[
|
||||||
//console.log('[Controller.setup.<PUT request>] Request:', req);
|
...(Reflect.getMetadata('middlewares', target) as RequestHandler[]),
|
||||||
//console.log('[Controller.setup.<PUT request>] Response:', res);
|
...middleware,
|
||||||
//console.log('[Controller.setup.<PUT request>] Next:', next);
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
//console.log('[Controller.setup.<PUT request>] Request:', req);
|
||||||
|
//console.log('[Controller.setup.<PUT request>] Response:', res);
|
||||||
|
//console.log('[Controller.setup.<PUT request>] Next:', next);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([fn.bind(controller)(req, res, next)]);
|
await Promise.all([fn.bind(controller)(req, res, next)]);
|
||||||
//console.log('[Controller.setup.<PUT request>] Successfully executed PUT method:', method);
|
//console.log('[Controller.setup.<PUT request>] Successfully executed PUT method:', method);
|
||||||
}
|
|
||||||
catch(error) {
|
|
||||||
//console.log('[Controller.setup.<PUT request>] Error:', error);
|
|
||||||
if(typeof next !== 'undefined') {
|
|
||||||
//console.log('[Controller.setup.<PUT request>] Calling next with error:', error);
|
|
||||||
|
|
||||||
next(error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
else {
|
catch(error) {
|
||||||
//console.log('[Controller.setup.<PUT request>] No next function defined, rejecting promise with error:', error);
|
//console.log('[Controller.setup.<PUT request>] Error:', error);
|
||||||
|
if(typeof next !== 'undefined') {
|
||||||
|
//console.log('[Controller.setup.<PUT request>] Calling next with error:', error);
|
||||||
|
|
||||||
// Because next is undefined and we still want to have Express handle the error we reject the promise
|
next(error);
|
||||||
return Promise.reject(error);
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//console.log('[Controller.setup.<PUT 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 `@DELETE` decorator
|
// Loop over all the methods in the decorated class looking for methods that use the `@DELETE` decorator
|
||||||
|
|
@ -207,32 +295,38 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
|
||||||
const path = deleteRoute.path;
|
const path = deleteRoute.path;
|
||||||
const middleware = deleteRoute.middleware;
|
const middleware = deleteRoute.middleware;
|
||||||
|
|
||||||
|
const fullPath = Reflect.getMetadata(DELETE_FORCE_PREFIX_METADATA_KEY, target.prototype, method) ? this.joinPaths(currentPrefix, path) : path;
|
||||||
|
|
||||||
// Bind the method to the class instance
|
// Bind the method to the class instance
|
||||||
app.delete(path, ...middleware, async (req, res, next) => {
|
app.delete(fullPath, ...[
|
||||||
//console.log('[Controller.setup.<DELETE request>] Request:', req);
|
...(Reflect.getMetadata('middlewares', target) as RequestHandler[]),
|
||||||
//console.log('[Controller.setup.<DELETE request>] Response:', res);
|
...middleware,
|
||||||
//console.log('[Controller.setup.<DELETE request>] Next:', next);
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
//console.log('[Controller.setup.<DELETE request>] Request:', req);
|
||||||
|
//console.log('[Controller.setup.<DELETE request>] Response:', res);
|
||||||
|
//console.log('[Controller.setup.<DELETE request>] Next:', next);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([fn.bind(controller)(req, res, next)]);
|
await Promise.all([fn.bind(controller)(req, res, next)]);
|
||||||
//console.log('[Controller.setup.<DELETE request>] Successfully executed DELETE method:', method);
|
//console.log('[Controller.setup.<DELETE request>] Successfully executed DELETE method:', method);
|
||||||
|
}
|
||||||
|
catch(error) {
|
||||||
|
//console.log('[Controller.setup.<DELETE request>] Error:', error);
|
||||||
|
if(typeof next !== 'undefined') {
|
||||||
|
//console.log('[Controller.setup.<DELETE request>] Calling next with error:', error);
|
||||||
|
|
||||||
|
next(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//console.log('[Controller.setup.<DELETE 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(error) {
|
]);
|
||||||
//console.log('[Controller.setup.<DELETE request>] Error:', error);
|
|
||||||
if(typeof next !== 'undefined') {
|
|
||||||
//console.log('[Controller.setup.<DELETE request>] Calling next with error:', error);
|
|
||||||
|
|
||||||
next(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//console.log('[Controller.setup.<DELETE 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { NextFunction } from 'express';
|
||||||
import type { NextHandleFunction } from 'connect';
|
import type { NextHandleFunction } from 'connect';
|
||||||
|
|
||||||
export const DELETE_METADATA_KEY = 'Delete';
|
export const DELETE_METADATA_KEY = 'Delete';
|
||||||
|
export const DELETE_FORCE_PREFIX_METADATA_KEY = 'DeleteForcePrefix';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||||
|
|
@ -11,11 +12,13 @@ export const DELETE_METADATA_KEY = 'Delete';
|
||||||
* Controller authors can also specify middleware to be used with the DELETE route (because a middleware is commonly required to parse the body of a DELETE request).
|
* Controller authors can also specify middleware to be used with the DELETE route (because a middleware is commonly required to parse the body of a DELETE request).
|
||||||
*
|
*
|
||||||
* @param path The path for the DELETE route.
|
* @param path The path for the DELETE route.
|
||||||
|
* @param forcePrefix Whether to force the appropriate prefix to be applied to the route's path. Default is true.
|
||||||
* @param middleware The middleware to use with the DELETE route.
|
* @param middleware The middleware to use with the DELETE route.
|
||||||
*/
|
*/
|
||||||
export function DELETE(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
export function DELETE(path: string = '', forcePrefix: boolean = true, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
// Define a metadata key with the path and middleware as the value on the target's propertyKey
|
// Define a metadata key with the path and middleware as the value on the target's propertyKey
|
||||||
Reflect.defineMetadata(DELETE_METADATA_KEY, { path, middleware }, target, propertyKey);
|
Reflect.defineMetadata(DELETE_METADATA_KEY, { path, middleware }, target, propertyKey);
|
||||||
|
Reflect.defineMetadata(DELETE_FORCE_PREFIX_METADATA_KEY, forcePrefix, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export const GET_METADATA_KEY = 'Get';
|
export const GET_METADATA_KEY = 'Get';
|
||||||
|
export const GET_FORCE_PREFIX_METADATA_KEY = 'GetForcePrefix';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||||
|
|
@ -7,10 +8,12 @@ export const GET_METADATA_KEY = 'Get';
|
||||||
* The specific path for the route is defined by the path parameter.
|
* The specific path for the route is defined by the path parameter.
|
||||||
*
|
*
|
||||||
* @param path The path for the GET route.
|
* @param path The path for the GET route.
|
||||||
|
* @param forcePrefix Whether to force the appropriate prefix to be applied to the route's path. Default is true.
|
||||||
*/
|
*/
|
||||||
export function GET(path: string) {
|
export function GET(path: string = '', forcePrefix: boolean = true) {
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
// Define a `Get` metadata key with the path as the value on the target's propertyKey
|
// Define a `Get` metadata key with the path as the value on the target's propertyKey
|
||||||
Reflect.defineMetadata(GET_METADATA_KEY, path, target, propertyKey);
|
Reflect.defineMetadata(GET_METADATA_KEY, path, target, propertyKey);
|
||||||
|
Reflect.defineMetadata(GET_FORCE_PREFIX_METADATA_KEY, forcePrefix, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import type { NextFunction } from 'express';
|
import type { NextFunction } from 'express';
|
||||||
import type { NextHandleFunction } from 'connect';
|
import type { NextHandleFunction } from 'connect';
|
||||||
|
|
||||||
export const POST_METADATA_KEY = 'Put';
|
export const POST_METADATA_KEY = 'Post';
|
||||||
|
export const POST_FORCE_PREFIX_METADATA_KEY = 'PostForcePrefix';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||||
|
|
@ -11,11 +12,13 @@ export const POST_METADATA_KEY = 'Put';
|
||||||
* Controller authors can also specify middleware to be used with the POST route (because a middleware is commonly required to parse the body of a POST request).
|
* Controller authors can also specify middleware to be used with the POST route (because a middleware is commonly required to parse the body of a POST request).
|
||||||
*
|
*
|
||||||
* @param path The path for the GET route.
|
* @param path The path for the GET route.
|
||||||
|
* @param forcePrefix Whether to force the appropriate prefix to be applied to the route's path.
|
||||||
* @param middleware The middleware to use with the POST route.
|
* @param middleware The middleware to use with the POST route.
|
||||||
*/
|
*/
|
||||||
export function POST(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
export function POST(path: string = '', forcePrefix: boolean = true, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
// Define a `Get` metadata key with the path as the value on the target's propertyKey
|
// Define a `Get` metadata key with the path as the value on the target's propertyKey
|
||||||
Reflect.defineMetadata(POST_METADATA_KEY, { path, middleware }, target, propertyKey);
|
Reflect.defineMetadata(POST_METADATA_KEY, { path, middleware }, target, propertyKey);
|
||||||
|
Reflect.defineMetadata(POST_FORCE_PREFIX_METADATA_KEY, forcePrefix, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import type { NextFunction } from 'express';
|
||||||
import type { NextHandleFunction } from 'connect';
|
import type { NextHandleFunction } from 'connect';
|
||||||
|
|
||||||
export const PUT_METADATA_KEY = 'Put';
|
export const PUT_METADATA_KEY = 'Put';
|
||||||
|
export const PUT_FORCE_PREFIX_METADATA_KEY = 'PutForcePrefix';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||||
|
|
@ -11,11 +12,13 @@ export const PUT_METADATA_KEY = 'Put';
|
||||||
* Controller authors can also specify middleware to be used with the PUT route (because a middleware is commonly required to parse the body of a PUT request).
|
* Controller authors can also specify middleware to be used with the PUT route (because a middleware is commonly required to parse the body of a PUT request).
|
||||||
*
|
*
|
||||||
* @param path The path for the PUT route.
|
* @param path The path for the PUT route.
|
||||||
|
* @param forcePrefix Whether to force the appropriate prefix to be applied to the route's path. Default is true.
|
||||||
* @param middleware The middleware to use with the PUT route.
|
* @param middleware The middleware to use with the PUT route.
|
||||||
*/
|
*/
|
||||||
export function PUT(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
export function PUT(path: string = '', forcePrefix: boolean = true, ...middleware: (NextHandleFunction | NextFunction)[]) {
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
// Define a metadata key with the path and middleware as the value on the target's propertyKey
|
// Define a metadata key with the path and middleware as the value on the target's propertyKey
|
||||||
Reflect.defineMetadata(PUT_METADATA_KEY, { path, middleware }, target, propertyKey);
|
Reflect.defineMetadata(PUT_METADATA_KEY, { path, middleware }, target, propertyKey);
|
||||||
|
Reflect.defineMetadata(PUT_FORCE_PREFIX_METADATA_KEY, forcePrefix, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Request, Response, NextFunction } from 'express';
|
import type { Response } from 'express';
|
||||||
|
|
||||||
// Response type guard
|
// Response type guard
|
||||||
function isResponse(obj) {
|
function isResponse(obj) {
|
||||||
|
|
@ -36,12 +36,34 @@ function isResponse(obj) {
|
||||||
* @param otherParams Any other parameters to pass to the page
|
* @param otherParams Any other parameters to pass to the page
|
||||||
*/
|
*/
|
||||||
export function Page(title: string, page: string, extraScripts: (string | { script: string, defer: boolean })[] = [], extraStyles: string[] = [], ...otherParams: any[]) {
|
export function Page(title: string, page: string, extraScripts: (string | { script: string, defer: boolean })[] = [], extraStyles: string[] = [], ...otherParams: any[]) {
|
||||||
|
/** Extract the response object from the arguments */
|
||||||
|
const getResponseFromArgs = (propertyKey: string, ...args: any[]) => {
|
||||||
|
// Find the response object in the arguments
|
||||||
|
const responseArgs = args.filter(arg => isResponse(arg));
|
||||||
|
|
||||||
|
// Verify there is only one response object
|
||||||
|
if (responseArgs.length !== 1) {
|
||||||
|
console.warn(`Page decorator: Incorrect number of response objects found in arguments for ${propertyKey}. Should ONLY be one.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = responseArgs[0] as Response;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render the page with the given parameters */
|
||||||
const doRender = (propertyKey: string, output: any, ...args: any[]) => {
|
const doRender = (propertyKey: string, output: any, ...args: any[]) => {
|
||||||
if (!args.some(arg => isResponse(arg)) || args.filter(arg => isResponse(arg)).length > 1) {
|
// Get the response object from the arguments
|
||||||
console.warn(`Page decorator: Incorrect number of response objects found in arguments for ${propertyKey}. Should ONLY be one response object.`);
|
const res = getResponseFromArgs(propertyKey, ...args);
|
||||||
|
|
||||||
|
// Verify there is a response object
|
||||||
|
if (res === null) {
|
||||||
|
console.warn(`Page decorator: Can't find response object for ${propertyKey}. Aborting render.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parameters from the decorator to pass as rendering parameters
|
||||||
const renderParams: { [key: string]: any } = {
|
const renderParams: { [key: string]: any } = {
|
||||||
title: title,
|
title: title,
|
||||||
page: page,
|
page: page,
|
||||||
|
|
@ -50,107 +72,63 @@ export function Page(title: string, page: string, extraScripts: (string | { scri
|
||||||
...otherParams
|
...otherParams
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the decorated method's output is an object, we want to merge it with the renderParams
|
// If the decorated method's output is an object,
|
||||||
if(typeof output === 'object') {
|
// we want to merge it with the parameters from the decorator
|
||||||
Object.entries(output).forEach((entry) => {
|
// Note, this does allow overriding of the decorator parameters from function output
|
||||||
renderParams[entry[0]] = entry[1];
|
if(typeof output === 'object' && output !== null) {
|
||||||
});
|
Object.assign(renderParams, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.find(arg => isResponse(arg)).render('base', renderParams);
|
res.render('base', renderParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
|
// The "original" (decorated) method
|
||||||
const original = descriptor.value;
|
const original = descriptor.value;
|
||||||
|
|
||||||
// If the original function has more than 3 parameters, we can assume it is an error handler
|
descriptor.value = async function (...args: any[]) {
|
||||||
// An error handler starts with the error object, then the request, response and next functions.
|
// Extract the `next` function from the arguments (if it exists)
|
||||||
if(original.length > 3) {
|
// This handles both (req, res, next) and (err, req, res, next) signatures dynamically
|
||||||
descriptor.value = async function (err: Error, req: Request, res: Response, next: NextFunction, ...args: any[]) {
|
const nextIndex = args.findIndex(arg => typeof arg === 'function');
|
||||||
//console.log('[Page Decorator] Error:', err);
|
const next = nextIndex !== -1 ? args[nextIndex] : undefined;
|
||||||
//console.log('[Page Decorator] Request:', req);
|
|
||||||
//console.log('[Page Decorator] Response:', res);
|
|
||||||
//console.log('[Page Decorator] Next:', next);
|
|
||||||
|
|
||||||
try {
|
//if(args.length > 3) {
|
||||||
// We run the original here so that if the decorated method has specific checks it needs to make (ex. if the ID of whatever actually exists) it can make them before rendering the page
|
// console.log('[Page Decorator] Error:', args[0]);
|
||||||
//
|
//}
|
||||||
// !!!IMPORTANT!!!
|
//console.log('[Page Decorator] Request:', args.length > 3 ? args[1] : args[0]);
|
||||||
// This currently is kind of a hacky solution, because we bind to the target, which is the prototype rather than the instance.
|
//console.log('[Page Decorator] Response:', args.length > 3 ? args[2] : args[1]);
|
||||||
// This means instance data is not properly available/accessible within the decorated method.
|
//console.log('[Page Decorator] Next:', next);
|
||||||
// However, you can use the `this` keyword to access other methods on the class, etc...
|
|
||||||
//
|
|
||||||
// The above (this approach) is necessary at time of writing because the `this` (which should be bound to for the instance) seems to be undefined within the context of the decorator function here for some reason
|
|
||||||
// Ideally, would dive into this and do it properly but this seems to be good enough for now.
|
|
||||||
const output = await original.bind(target)(err, req, res, next, ...args);
|
|
||||||
|
|
||||||
// If the output is false, we don't want to render the page
|
try {
|
||||||
//
|
// We run the original (decorated) function here.
|
||||||
// This allows the decorated method to handle the response itself
|
// This is so that it can pass any needed parameters to the rendered page and/or stop rendering (return `false`)
|
||||||
// Ex. Logging in, if the user is already logged in we want to redirect them, not render the login page again
|
// Note, we `.apply` to `this` preserving instance context
|
||||||
if(typeof output === 'boolean' && !output) {
|
const output = await original.apply(this, args);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender(propertyKey, output, err, req, res, next, ...args);
|
// If the output is false, we don't want to render the page
|
||||||
|
//
|
||||||
|
// This allows the decorated method to handle the response itself
|
||||||
|
// Ex. Logging in, if the user is already logged in we want to redirect them, not render the login page again
|
||||||
|
if(typeof output === 'boolean' && !output) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch(error) {
|
|
||||||
//console.error('[Page Decorator] Error:', error);
|
|
||||||
|
|
||||||
if(typeof next !== 'undefined') {
|
doRender(propertyKey, output, ...args);
|
||||||
next(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Because next is undefined and we still want to have Express handle the error we reject the promise
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
catch(error) {
|
||||||
else {
|
//console.error(`[Page Decorator] Error while rendering page: ${error}`);
|
||||||
descriptor.value = async function (req: Request, res: Response, next: NextFunction) {
|
|
||||||
//console.log('[Page Decorator] Request:', req);
|
|
||||||
//console.log('[Page Decorator] Response:', res);
|
|
||||||
//console.log('[Page Decorator] Next:', next);
|
|
||||||
|
|
||||||
try {
|
if(typeof next !== 'undefined') {
|
||||||
// We run the original here so that if the decorated method has specific checks it needs to make (ex. if the ID of whatever actually exists) it can make them before rendering the page
|
//console.log(`[Page Decorator] Calling next with error: ${error}`);
|
||||||
//
|
|
||||||
// !!!IMPORTANT!!!
|
|
||||||
// This currently is kind of a hacky solution, because we bind to the target, which is the prototype rather than the instance.
|
|
||||||
// This means instance data is not properly available/accessible within the decorated method.
|
|
||||||
// However, you can use the `this` keyword to access other methods on the class, etc...
|
|
||||||
//
|
|
||||||
// The above (this approach) is necessary at time of writing because the `this` (which should be bound to for the instance) seems to be undefined within the context of the decorator function here for some reason
|
|
||||||
// Ideally, would dive into this and do it properly but this seems to be good enough for now.
|
|
||||||
const output = await original.bind(target)(req, res, next);
|
|
||||||
|
|
||||||
// If the output is false, we don't want to render the page
|
next(error);
|
||||||
//
|
return;
|
||||||
// This allows the decorated method to handle the response itself
|
|
||||||
// Ex. Logging in, if the user is already logged in we want to redirect them, not render the login page again
|
|
||||||
if(typeof output === 'boolean' && !output) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender(propertyKey, output, req, res, next);
|
|
||||||
}
|
}
|
||||||
catch(error) {
|
else {
|
||||||
//console.error('[Page Decorator] Error:', error);
|
//console.log('[Page Decorator] No next function defined, rejecting promise with error:', error);
|
||||||
|
|
||||||
if(typeof next !== 'undefined') {
|
// Because next is undefined and we still want to have Express handle the error we reject the promise
|
||||||
//console.log('[Page Decorator] Calling next with error:', error);
|
return Promise.reject(error);
|
||||||
|
|
||||||
next(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//console.log('[Page Decorator] 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue