Initial Code Commit
This commit is contained in:
parent
fa1468245f
commit
28db152b87
24 changed files with 2691 additions and 2 deletions
8
src/decorators/ChildController.ts
Normal file
8
src/decorators/ChildController.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { BaseController } from '../controllers/BaseController';
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
95
src/decorators/Controller.ts
Normal file
95
src/decorators/Controller.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { Application, NextFunction } from 'express';
|
||||
|
||||
import { BaseController } from '../controllers/BaseController';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ```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';
|
||||
*
|
||||
* @Controller()
|
||||
* export class MyController extends BaseController {
|
||||
* @GET('/path')
|
||||
* private myGetMethod(req: Request, res: Response) {}
|
||||
*
|
||||
* @POST('/path', express.json())
|
||||
* private myPostMethod(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('ChildController', 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', target.prototype, method))
|
||||
.map((method) => {
|
||||
// Get the method
|
||||
const fn = target.prototype[method];
|
||||
|
||||
// Get the path
|
||||
const path = Reflect.getMetadata('Get', target.prototype, method);
|
||||
|
||||
// Bind the method to the class instance
|
||||
app.get(path, fn.bind(controller));
|
||||
});
|
||||
|
||||
// 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))
|
||||
.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 path = postRoute.path;
|
||||
const middleware: NextFunction[] = postRoute.middleware;
|
||||
|
||||
// Bind the method to the class instance
|
||||
app.post(path, ...middleware, fn.bind(controller));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/decorators/GET.ts
Normal file
14
src/decorators/GET.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||
*
|
||||
* This in conjunction with the `@Controller` decorator will automatically setup a GET route that executes the decorated method.
|
||||
* The specific path for the route is defined by the path parameter.
|
||||
*
|
||||
* @param path The path for the GET route.
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
19
src/decorators/POST.ts
Normal file
19
src/decorators/POST.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { NextFunction } from 'express';
|
||||
import { NextHandleFunction } from 'connect';
|
||||
|
||||
/**
|
||||
* Method decorator intended to be used on methods of a class decorated with the `@Controller` decorator.
|
||||
*
|
||||
* This in conjunction with the `@Controller` decorator will automatically setup a POST route that executes the decorated method.
|
||||
* The specific path for the route is defined by the path parameter.
|
||||
* 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 middleware The middleware to use with the POST route.
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
45
src/decorators/Page.ts
Normal file
45
src/decorators/Page.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Request, Response } from 'express';
|
||||
|
||||
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) {
|
||||
// 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!!!
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const renderParams: { [key: string]: any } = {
|
||||
title: title,
|
||||
page: page,
|
||||
extraStyles: extraStyles,
|
||||
extraScripts: extraScripts,
|
||||
...otherParams
|
||||
};
|
||||
|
||||
// If the decorated method's output is an object, we want to merge it with the renderParams
|
||||
if(typeof output === 'object') {
|
||||
Object.entries(output).forEach((entry) => {
|
||||
renderParams[entry[0]] = entry[1];
|
||||
});
|
||||
}
|
||||
|
||||
res.render('base', renderParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/decorators/index.ts
Normal file
13
src/decorators/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Controller } from './Controller';
|
||||
import { ChildController } from './ChildController';
|
||||
import { Page } from './Page';
|
||||
import { GET } from './GET';
|
||||
import { POST } from './POST';
|
||||
|
||||
export {
|
||||
Controller,
|
||||
ChildController,
|
||||
Page,
|
||||
GET,
|
||||
POST
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue