Bringing local and remote repository in line with one another again

This commit is contained in:
Alan Bridgeman 2025-04-30 14:58:07 -05:00
parent bc581f9eac
commit 332e177f99
15 changed files with 1344 additions and 222 deletions

View file

@ -1,8 +1,10 @@
import { BaseController } from '../controllers/BaseController';
export const CHILD_CONTROLLER_METADATA_KEY = 'ChildController';
export function ChildController<T extends { new(...args: any[]): {} }>(childController: BaseController | BaseController[]) {
return function (target: T) {
// Define a metadata key for the child controller
Reflect.defineMetadata('ChildController', childController, target);
Reflect.defineMetadata(CHILD_CONTROLLER_METADATA_KEY, childController, target);
};
}

View file

@ -2,29 +2,37 @@ import { Application, 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 and POST method.
* 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 } from '../decorators/Controller';
* import { GET } from '../decorators/GET';
* import { POST } from '../decorators/POST';
*
* import { BaseController } from './BaseController';
*
*
* 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) {}
* }
* ```
*/
@ -45,7 +53,7 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
//
// 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('ChildController', target);
const childControllers = Reflect.getMetadata(CHILD_CONTROLLER_METADATA_KEY, target);
if(typeof childControllers !== 'undefined') {
if(Array.isArray(childControllers)) {
childControllers.forEach((childController) => {
@ -61,13 +69,13 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
// 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', target.prototype, method))
.filter((method) => Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method))
.map((method) => {
// Get the method
const fn = target.prototype[method];
// Get the path
const path = Reflect.getMetadata('Get', target.prototype, method);
const path = Reflect.getMetadata(GET_METADATA_KEY, target.prototype, method);
// Bind the method to the class instance
app.get(path, fn.bind(controller));
@ -76,19 +84,53 @@ export function Controller<T extends { new (...args: any[]): BaseController }>()
// 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', target.prototype, method))
.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', target.prototype, method);
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));
});
}
}
}

View file

@ -1,3 +1,5 @@
export const GET_METADATA_KEY = 'Get';
/**
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
*
@ -9,6 +11,6 @@
export function GET(path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Define a `Get` metadata key with the path as the value on the target's propertyKey
Reflect.defineMetadata('Get', path, target, propertyKey);
Reflect.defineMetadata(GET_METADATA_KEY, path, target, propertyKey);
};
}

View file

@ -1,6 +1,8 @@
import { NextFunction } from 'express';
import { NextHandleFunction } from 'connect';
export const POST_METADATA_KEY = 'Put';
/**
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
*
@ -14,6 +16,6 @@ import { NextHandleFunction } from 'connect';
export function POST(path: string, ...middleware: (NextHandleFunction | NextFunction)[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Define a `Get` metadata key with the path as the value on the target's propertyKey
Reflect.defineMetadata('Post', { path, middleware }, target, propertyKey);
Reflect.defineMetadata(POST_METADATA_KEY, { path, middleware }, target, propertyKey);
};
}

View file

@ -1,10 +1,50 @@
import { Request, Response } from 'express';
// Response type guard
function isResponse(obj) {
return obj && typeof obj.render === 'function';
}
/**
* The `Page` decorator allows for easy rendering of a page.
*
* Note, that the parameters for the function the page decorator is placed on is not restrictive.
* This is because JavaScript doesn't have the idea of named arguments so a request handler like:
*
* ```ts
* function handle(req: Request, res: Response)
* ```
*
* Is different from a middleware handler like:
*
* ```ts
* function handle(req: Request, res: Response, next: NextFunction)
* ```
*
* Is different from an error handler like:
*
* ```ts
* function handle(err: Error, req: Request, res: Response, next: NextFunction)
* ```
*
* Despite them all being, more or less, equivalent for the page decorator's usage.
*
* @param title The title of the page
* @param page The name of the page file to render
* @param extraScripts Any extra scripts to include in the page
* @param extraStyles Any extra styles to include in 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[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (req: Request, res: Response) {
descriptor.value = async function (...args: any[]) {
if (!args.some(arg => isResponse(arg)) || args.filter(arg => isResponse(arg)).length > 1) {
console.warn(`Page decorator: Incorrect number of response objects found in arguments for ${propertyKey}. Should ONLY be one response object.`);
return;
}
// 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
//
// !!!IMPORTANT!!!
@ -14,7 +54,7 @@ export function Page(title: string, page: string, extraScripts: (string | { scri
//
// 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);
const output = await original.bind(target)(...args);
// If the output is false, we don't want to render the page
//
@ -39,7 +79,7 @@ export function Page(title: string, page: string, extraScripts: (string | { scri
});
}
res.render('base', renderParams);
args.find(arg => isResponse(arg)).render('base', renderParams);
}
}
}

View file

@ -3,11 +3,17 @@ import { ChildController } from './ChildController';
import { Page } from './Page';
import { GET } from './GET';
import { POST } from './POST';
import { PUT } from './PUT';
import { DELETE } from './DELETE';
import { ErrorHandler } from './ErrorHandler';
export {
Controller,
ChildController,
Page,
GET,
POST
}
export {
Controller,
ChildController,
Page,
GET,
POST,
PUT,
DELETE,
ErrorHandler
};