Bringing local and remote repository in line with one another again
This commit is contained in:
parent
bc581f9eac
commit
332e177f99
15 changed files with 1344 additions and 222 deletions
53
.github/workflows/publish.yml
vendored
53
.github/workflows/publish.yml
vendored
|
|
@ -18,6 +18,10 @@ jobs:
|
||||||
- name: Set up NPM Auth Token
|
- name: Set up NPM Auth Token
|
||||||
run: echo "NODE_AUTH_TOKEN=${{ secrets.NPM_TOKEN }}" >> $GITHUB_ENV
|
run: echo "NODE_AUTH_TOKEN=${{ secrets.NPM_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Set up NPM Auth Token
|
||||||
|
- name: Set up NPM Auth Token
|
||||||
|
run: echo "NODE_AUTH_TOKEN=${{ secrets.NPM_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
# Set up Node.js
|
# Set up Node.js
|
||||||
- name: Set up Node.js version
|
- name: Set up Node.js version
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
@ -42,12 +46,53 @@ jobs:
|
||||||
# > Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/).
|
# > Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/).
|
||||||
scope: '@BridgemanAccessible'
|
scope: '@BridgemanAccessible'
|
||||||
|
|
||||||
# Install dependencies
|
# Transpile/Build the package (TypeScript -> JavaScript)
|
||||||
- name: Install dependencies
|
- name: Transpile/Build the package (TypeScript -> JavaScript)
|
||||||
run: |
|
run: |
|
||||||
|
# Install needed dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
|
# Build the package
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
|
- name: Determine Version and Increment (if needed)
|
||||||
|
id: version_check
|
||||||
|
run: |
|
||||||
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
NAME=$(node -p "require('./package.json').name")
|
||||||
|
LATEST_VERSION=$(npm show $NAME version --registry https://npm.pkg.bridgemanaccessible.ca)
|
||||||
|
echo "Latest version: $LATEST_VERSION"
|
||||||
|
|
||||||
|
if [ "$LATEST_VERSION" != "$VERSION" ]; then
|
||||||
|
echo "Manually updated version detected: $VERSION"
|
||||||
|
else
|
||||||
|
NEW_VERSION=$(npm version patch --no-git-tag-version)
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
echo "new_version=$NEW_VERSION" >> $GITHUB_ENV
|
||||||
|
echo "version_changed=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit Version Change (if needed)
|
||||||
|
if: steps.version_check.outputs.version_changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git add package.json
|
||||||
|
git commit -m "[Github Actions] Update version to ${{ env.new_version }}"
|
||||||
|
git push origin HEAD:main
|
||||||
|
|
||||||
# Publish to private NPM registry
|
# Publish to private NPM registry
|
||||||
- name: Publish package
|
- name: Publish the package
|
||||||
run: npm publish
|
run: |
|
||||||
|
# Copy over the files to the build output (`dist`) folder
|
||||||
|
cp package.json dist/package.json
|
||||||
|
cp README.md dist/README.md
|
||||||
|
cp LICENSE dist/LICENSE
|
||||||
|
|
||||||
|
# Change directory to the build output (`dist`) folder
|
||||||
|
cd dist
|
||||||
|
|
||||||
|
# Publish the package to the private NPM registry
|
||||||
|
npm publish --registry http://npm.pkg.bridgemanaccessible.ca/
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -51,6 +51,8 @@ web_modules/
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
.npm
|
.npm
|
||||||
|
|
||||||
|
.npmrc
|
||||||
|
|
||||||
# Optional eslint cache
|
# Optional eslint cache
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"republish": "yarn build && cp package.json dist/package.json && cd dist && npm unpublish --registry https://npm.pkg.bridgemanaccessible.ca --force @BridgemanAccessible/ba-web-framework@1.0.0 && npm publish --registry https://npm.pkg.bridgemanaccessible.ca && cd ../"
|
"republish": "yarn build && cp package.json dist/package.json && cd dist && npm unpublish --registry https://npm.pkg.bridgemanaccessible.ca --force @BridgemanAccessible/ba-web-framework@1.0.0 && npm publish --registry https://npm.pkg.bridgemanaccessible.ca && cd ../"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@BridgemanAccessible/ba-auth": "^1.0.0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.1.0",
|
||||||
|
|
|
||||||
34
src/App.ts
34
src/App.ts
|
|
@ -10,28 +10,50 @@ export class App {
|
||||||
/** The default port to run the server on (if the environment variable isn't set) */
|
/** The default port to run the server on (if the environment variable isn't set) */
|
||||||
private readonly DEFAULT_PORT = 3000;
|
private readonly DEFAULT_PORT = 3000;
|
||||||
|
|
||||||
|
private initializer?: Initializer;
|
||||||
|
|
||||||
|
getInitializer() {
|
||||||
|
if(typeof this.initializer === 'undefined') {
|
||||||
|
throw new Error('Initializer is not set. Please call run() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.initializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpressApp() {
|
||||||
|
return this.getInitializer().getExpressApp();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main entry point for the web app
|
* The main entry point for the web app
|
||||||
* This is mostly required because of async/await
|
* This is mostly required because of async/await
|
||||||
*/
|
*/
|
||||||
async run<T extends Initializer>(initializer?: T, callback?: (app: Application) => void | Promise<void>) {
|
async run<T extends Initializer>(initializer?: T, callback?: (app: App) => void | Promise<void>, onErrorCallback?: (error: any) => void | Promise<void>) {
|
||||||
// Do the initial setup of the app
|
// Do the initial setup of the app
|
||||||
let app: Application;
|
|
||||||
if(typeof initializer !== 'undefined') {
|
if(typeof initializer !== 'undefined') {
|
||||||
app = await initializer.init();
|
this.initializer = initializer;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
app = await (new Initializer()).init();
|
this.initializer = new Initializer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.initializer.init();
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
const port = process.env.PORT || this.DEFAULT_PORT;
|
const port = process.env.PORT || this.DEFAULT_PORT;
|
||||||
app.listen(port, async () => {
|
this.getExpressApp().listen(port, async () => {
|
||||||
console.log(`Server is running on port ${port}`);
|
console.log(`Server is running on port ${port}`);
|
||||||
|
|
||||||
// Run the callback if one is provided
|
// Run the callback if one is provided
|
||||||
if(typeof callback !== 'undefined') {
|
if(typeof callback !== 'undefined') {
|
||||||
await callback(app);
|
await callback.bind(this)(this);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('error', async (error) => {
|
||||||
|
console.error('Error starting server:', error);
|
||||||
|
|
||||||
|
if(typeof onErrorCallback !== 'undefined') {
|
||||||
|
await onErrorCallback(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ export class Initializer {
|
||||||
/** The middlewares to use */
|
/** The middlewares to use */
|
||||||
private middlewares: ((...args: any[]) => RequestHandler)[];
|
private middlewares: ((...args: any[]) => RequestHandler)[];
|
||||||
|
|
||||||
|
private app?: Application;
|
||||||
|
private router?: Router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Initializer
|
* Create a new Initializer
|
||||||
*
|
*
|
||||||
|
|
@ -46,6 +49,22 @@ export class Initializer {
|
||||||
this.middlewares = middlewares;
|
this.middlewares = middlewares;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExpressApp() {
|
||||||
|
if(typeof this.app === 'undefined') {
|
||||||
|
throw new Error('App is not set. Please call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.app;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRouter() {
|
||||||
|
if(typeof this.router === 'undefined') {
|
||||||
|
throw new Error('Router is not set. Please call init() first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.router;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the Express app
|
* Create the Express app
|
||||||
*
|
*
|
||||||
|
|
@ -53,9 +72,9 @@ export class Initializer {
|
||||||
*/
|
*/
|
||||||
private async createExpressApp(): Promise<Application> {
|
private async createExpressApp(): Promise<Application> {
|
||||||
// Create the Express app
|
// Create the Express app
|
||||||
const app = express();
|
this.app = express();
|
||||||
|
|
||||||
return app;
|
return this.app;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,7 +91,7 @@ export class Initializer {
|
||||||
*
|
*
|
||||||
* @returns The Express app that gets created and setup
|
* @returns The Express app that gets created and setup
|
||||||
*/
|
*/
|
||||||
async init(): Promise<Application> {
|
async init() {
|
||||||
// Create the Express app
|
// Create the Express app
|
||||||
const app = await this.createExpressApp();
|
const app = await this.createExpressApp();
|
||||||
|
|
||||||
|
|
@ -81,11 +100,12 @@ export class Initializer {
|
||||||
|
|
||||||
// Setup the router (how the app handles requests)
|
// Setup the router (how the app handles requests)
|
||||||
if(typeof this.controllersPath !== 'undefined') {
|
if(typeof this.controllersPath !== 'undefined') {
|
||||||
await (new Router(this.controllersPath)).setup(app);
|
this.router = new Router(this.controllersPath);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await (new Router()).setup(app);
|
this.router = new Router();
|
||||||
}
|
}
|
||||||
|
await this.router.setup(app);
|
||||||
|
|
||||||
// Setup the static file resolver (how the app serves static files)
|
// Setup the static file resolver (how the app serves static files)
|
||||||
if(typeof this.staticFilesPath !== 'undefined') {
|
if(typeof this.staticFilesPath !== 'undefined') {
|
||||||
|
|
@ -107,7 +127,5 @@ export class Initializer {
|
||||||
else {
|
else {
|
||||||
await (new Renderer()).setup(app);
|
await (new Renderer()).setup(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +127,7 @@ export class OAuthApp extends App {
|
||||||
*
|
*
|
||||||
* @param app The app to setup the OAuth client for
|
* @param app The app to setup the OAuth client for
|
||||||
*/
|
*/
|
||||||
private async setupOAuthClient(app: Application) {
|
private async setupOAuthClient(app: App) {
|
||||||
// If the base URL of the app isn't provided, get it from the environment variable
|
// If the base URL of the app isn't provided, get it from the environment variable
|
||||||
let baseAppUrl = this.baseAppUrl;
|
let baseAppUrl = this.baseAppUrl;
|
||||||
if(typeof baseAppUrl === 'undefined') {
|
if(typeof baseAppUrl === 'undefined') {
|
||||||
|
|
@ -142,7 +142,7 @@ export class OAuthApp extends App {
|
||||||
|
|
||||||
console.log(`Attempting to create/register the app:\n\tApp Base URL: ${baseAppUrl}\n\tApp Abbreviation: ${appAbbrv}\n\tApp Name: ${typeof this.appName === 'undefined' ? 'uses APP_NAME environment variable (' + process.env.APP_NAME + ')' : this.appName}\n\tScopes: ${typeof this.scopes === 'undefined' ? 'uses SCOPES environment variable (' + process.env.SCOPES + ')' : this.scopes.join(', ')}`);
|
console.log(`Attempting to create/register the app:\n\tApp Base URL: ${baseAppUrl}\n\tApp Abbreviation: ${appAbbrv}\n\tApp Name: ${typeof this.appName === 'undefined' ? 'uses APP_NAME environment variable (' + process.env.APP_NAME + ')' : this.appName}\n\tScopes: ${typeof this.scopes === 'undefined' ? 'uses SCOPES environment variable (' + process.env.SCOPES + ')' : this.scopes.join(', ')}`);
|
||||||
|
|
||||||
await Client.setup(app, baseAppUrl, this.onAuth, this.saveSecret, appAbbrv, this.appName, this.scopes, {
|
const client = await Client.setup(app.getExpressApp(), baseAppUrl, this.onAuth, this.saveSecret, appAbbrv, this.appName, this.scopes, {
|
||||||
contacts: this.contacts,
|
contacts: this.contacts,
|
||||||
logo_url: this.logo_url,
|
logo_url: this.logo_url,
|
||||||
tos_url: this.tos_url,
|
tos_url: this.tos_url,
|
||||||
|
|
@ -153,6 +153,10 @@ export class OAuthApp extends App {
|
||||||
auth_default_response_mode: this.auth_default_response_mode,
|
auth_default_response_mode: this.auth_default_response_mode,
|
||||||
client_secret: this.client_secret
|
client_secret: this.client_secret
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.getSetupRoutes().forEach((route) => {
|
||||||
|
app.getInitializer().getRouter().addOutsideFrameworkRoute(route);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -164,7 +168,7 @@ export class OAuthApp extends App {
|
||||||
*
|
*
|
||||||
* @param app The Express app object (that is now listening)
|
* @param app The Express app object (that is now listening)
|
||||||
*/
|
*/
|
||||||
async onStart(app: Application, callback?: (app: Application) => void | Promise<void>) {
|
async onStart(app: App, callback?: (app: App) => void | Promise<void>) {
|
||||||
try {
|
try {
|
||||||
// Setup the OAuth client.
|
// Setup the OAuth client.
|
||||||
// This is done here because we need the client to be serving/listening for requests for the auth library stuff to work
|
// This is done here because we need the client to be serving/listening for requests for the auth library stuff to work
|
||||||
|
|
@ -192,7 +196,7 @@ export class OAuthApp extends App {
|
||||||
* And doesn't have to worry about the OAuth details to make this work.
|
* And doesn't have to worry about the OAuth details to make this work.
|
||||||
* Though it does provide tweaking the OAuth details via options provided to the constructor.
|
* Though it does provide tweaking the OAuth details via options provided to the constructor.
|
||||||
*/
|
*/
|
||||||
async run<T extends Initializer>(initializer?: T, callback?: (app: Application) => void | Promise<void>) {
|
async run<T extends Initializer>(initializer?: T, callback?: (app: App) => void | Promise<void>) {
|
||||||
await super.run(initializer, async (app: Application) => this.onStart(app, callback));
|
await super.run(initializer, async (app: App) => this.onStart(app, callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
263
src/Router.ts
263
src/Router.ts
|
|
@ -3,6 +3,7 @@ import { Application } from 'express';
|
||||||
import fse from 'fs-extra';
|
import fse from 'fs-extra';
|
||||||
|
|
||||||
import { BaseController } from './controllers/BaseController';
|
import { BaseController } from './controllers/BaseController';
|
||||||
|
import { ErrorController } from './controllers/ErrorController';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Router sets up the routes/endpoints for the App.
|
* The Router sets up the routes/endpoints for the App.
|
||||||
|
|
@ -19,6 +20,8 @@ export class Router {
|
||||||
/** The path to the controllers folder */
|
/** The path to the controllers folder */
|
||||||
private controllersPath: string;
|
private controllersPath: string;
|
||||||
|
|
||||||
|
private outsideFrameworkRoutes: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Router
|
* Create a new Router
|
||||||
*
|
*
|
||||||
|
|
@ -31,6 +34,14 @@ export class Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.controllersPath = path.resolve(controllersPath);
|
this.controllersPath = path.resolve(controllersPath);
|
||||||
|
|
||||||
|
this.outsideFrameworkRoutes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addOutsideFrameworkRoute(route: string) {
|
||||||
|
if(!this.outsideFrameworkRoutes.includes(route)) {
|
||||||
|
this.outsideFrameworkRoutes.push(route);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,26 +49,39 @@ export class Router {
|
||||||
*
|
*
|
||||||
* Note, if the specified "file" is a directory, this method will recurse into the directory and return all controllers found in the directory.
|
* Note, if the specified "file" is a directory, this method will recurse into the directory and return all controllers found in the directory.
|
||||||
*
|
*
|
||||||
|
* It's worth noting that typing in this method is deliberate but confusing.
|
||||||
|
* This is because decorators introduce a lot of complexity because they introduce this anonymous wrapper function/class around the original function/class.
|
||||||
|
* However, because of how we typed the decorator, we can use `instanceof` and hiarchtical typing.
|
||||||
|
* Though we do typing of variables that would otherwise be any and DON'T explicitly cast.
|
||||||
|
*
|
||||||
* @param file The file to check if it is a controller
|
* @param file The file to check if it is a controller
|
||||||
* @param folder The folder that the file is in
|
* @param folder The folder that the file is in
|
||||||
* @returns The controller class (or list of controller classes) in the "file" or null if the file is not a controller
|
* @returns An object with 2 properties: a `controllers` property which holds the controller class (or list of controller classes) in the "file" or null if the file is not a controller and `errorControllers` property which holds the error controller class (or list of error controller classes) in the "file" or null if the file is not an error controller.
|
||||||
*/
|
*/
|
||||||
private async checkIfControllerFile(file: string, folder: string): Promise<any /*BaseController*/ | any[]/*BaseController[]*/ | null> {
|
private async checkIfControllerFile(file: string, folder: string): Promise<{ controllers: BaseController | BaseController[] | null, errorControllers: (new (...args: any[]) => ErrorController) | (new (...args: any[]) => ErrorController)[] | null }> {
|
||||||
// Get the path to the specific file
|
// Get the path to the specific file
|
||||||
const controllerPath = path.join(folder, file);
|
const controllerPath = path.join(folder, file);
|
||||||
|
|
||||||
// Check if the path is a directory/folder
|
// Check if the path is a directory/folder and recurse if it is
|
||||||
const stats = await fse.stat(controllerPath);
|
const stats = await fse.stat(controllerPath);
|
||||||
if(stats.isDirectory()) {
|
if(stats.isDirectory()) {
|
||||||
// Recurse into the directory
|
// Recurse into the directory
|
||||||
const controllersFoundInDir = await Promise.all((await fse.readdir(controllerPath)).map(file => this.checkIfControllerFile(file, controllerPath)));
|
const controllersFoundInDir = await Promise.all((await fse.readdir(controllerPath)).map(file => this.checkIfControllerFile(file, controllerPath)));
|
||||||
const controllersInDir = controllersFoundInDir.filter(controller => controller !== null);
|
const controllersInDir = controllersFoundInDir.filter(controller => controller !== null);
|
||||||
return controllersInDir.length === 0 ? null : controllersInDir.flat();
|
if(controllersInDir.length === 0) {
|
||||||
|
return { controllers: null, errorControllers: null };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
controllers: controllersInDir.map(controllerInDir => controllerInDir.controllers).length > 1 ? controllersInDir.map(controllerInDir => controllerInDir.controllers).filter(ctlr => ctlr !== null).flat() : controllersInDir.map(controllerInDir => controllerInDir.controllers)[0],
|
||||||
|
errorControllers: controllersInDir.map(controllerInDir => controllerInDir.errorControllers).length > 1 ? controllersInDir.map(controllerInDir => controllerInDir.errorControllers).filter(ctlr => ctlr !== null).flat() : controllersInDir.map(controllerInDir => controllerInDir.errorControllers)[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only want to load JavaScript files (Note, Typescript files get compiled to JavaScript files)
|
// We only want to load JavaScript files (Note, Typescript files get compiled to JavaScript files)
|
||||||
if(!controllerPath.endsWith('.js')) {
|
if(!controllerPath.endsWith('.js')) {
|
||||||
return null;
|
return { controllers: null, errorControllers: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the file as a module
|
// Load the file as a module
|
||||||
|
|
@ -65,27 +89,57 @@ export class Router {
|
||||||
|
|
||||||
console.log(`${controllerPath} loaded successfully: ${JSON.stringify(Object.keys(controllerModule))} exports found.`)
|
console.log(`${controllerPath} loaded successfully: ${JSON.stringify(Object.keys(controllerModule))} exports found.`)
|
||||||
|
|
||||||
|
// Get all the classes in the module
|
||||||
|
const classes = Object.keys(controllerModule)
|
||||||
|
.filter(key => typeof controllerModule[key] === 'function' && /^\s*class\s+/.test(controllerModule[key].toString()));
|
||||||
|
|
||||||
// Get all the classes in the module that are controllers
|
// Get all the classes in the module that are controllers
|
||||||
//
|
//
|
||||||
// Note: We filter by if the class extends the `BaseController` class.
|
// Note: We filter by if the class extends the `BaseController` class.
|
||||||
// This is because native JavaScript doesn't have decorators (which are used to mark a class as a controller).
|
// This is because native JavaScript doesn't have decorators (which are used to mark a class as a controller).
|
||||||
// Consequently, we enforce that all controllers in addition to using the decorator must extend the `BaseController` class so that we can identify them.
|
// Consequently, we enforce that all controllers in addition to using the decorator must extend the `BaseController` class so that we can identify them.
|
||||||
// Further, note we can't do this as part of the decorator because of the way decorators get transpiled to JavaScript.
|
// Further, note we can't do this as part of the decorator because of the way decorators get transpiled to JavaScript.
|
||||||
const controllers = Object.keys(controllerModule)
|
const controllers: BaseController[] = classes
|
||||||
.filter(key => typeof controllerModule[key] === 'function' && /^\s*class\s+/.test(controllerModule[key].toString()))
|
|
||||||
.filter(exportedClassName => controllerModule[exportedClassName].prototype instanceof BaseController)
|
.filter(exportedClassName => controllerModule[exportedClassName].prototype instanceof BaseController)
|
||||||
.map(controllerClassName => {
|
.map(controllerClassName => {
|
||||||
return controllerModule[controllerClassName];
|
return controllerModule[controllerClassName];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get all the classes in the module that are error controllers (these are subtly different from normal controllers)
|
||||||
|
//
|
||||||
|
// We do this here, instead of braking it out into it's own separate method for efficiency reasons.
|
||||||
|
//
|
||||||
|
// Note: We filter by if the class extends the `ErrorController` class.
|
||||||
|
// This is because native JavaScript doesn't have decorators (which are used to mark a class as a error controller).
|
||||||
|
// Consequently, we enforce that all error controllers in addition to using the decorator must extend the `ErrorController` class so that we can identify them.
|
||||||
|
// Further, note we can't do this as part of the decorator because of the way decorators get transpiled to JavaScript.
|
||||||
|
const errorControllers: ErrorController[] = classes
|
||||||
|
.filter(exportedClassName => controllerModule[exportedClassName].prototype instanceof ErrorController)
|
||||||
|
.map(controllerClassName => {
|
||||||
|
return controllerModule[controllerClassName];
|
||||||
|
});
|
||||||
|
|
||||||
|
let foundControllers;
|
||||||
switch (controllers.length) {
|
switch (controllers.length) {
|
||||||
case 0:
|
case 0:
|
||||||
return null;
|
foundControllers = null;
|
||||||
case 1:
|
case 1:
|
||||||
return controllers[0];
|
foundControllers = controllers[0];
|
||||||
default:
|
default:
|
||||||
return controllers.flat();
|
foundControllers = controllers.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let foundErrorControllers;
|
||||||
|
switch (errorControllers.length) {
|
||||||
|
case 0:
|
||||||
|
foundErrorControllers = null;
|
||||||
|
case 1:
|
||||||
|
foundErrorControllers = errorControllers[0];
|
||||||
|
default:
|
||||||
|
foundErrorControllers = errorControllers.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { controllers: foundControllers, errorControllers: foundErrorControllers };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,6 +151,11 @@ export class Router {
|
||||||
* @returns The routes in the controller
|
* @returns The routes in the controller
|
||||||
*/
|
*/
|
||||||
private getRoutesInController(controller: any) {
|
private getRoutesInController(controller: any) {
|
||||||
|
if (typeof controller === 'undefined') {
|
||||||
|
console.error(`Something went wrong while processing ${JSON.stringify(controller)}`);
|
||||||
|
throw new Error('The controller must be a class');
|
||||||
|
}
|
||||||
|
|
||||||
// Loop over all the methods in the provided class looking for methods that use the GET decorator
|
// Loop over all the methods in the provided class looking for methods that use the GET decorator
|
||||||
const pathsGET = Object.getOwnPropertyNames(controller.prototype)
|
const pathsGET = Object.getOwnPropertyNames(controller.prototype)
|
||||||
// Find all methods that have a Get metadata key (GET decorator)
|
// Find all methods that have a Get metadata key (GET decorator)
|
||||||
|
|
@ -126,12 +185,89 @@ export class Router {
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Loop over all the methods in the provided class looking for methods that use the `@PUT` decorator
|
||||||
|
const pathsPUT = Object.getOwnPropertyNames(controller.prototype)
|
||||||
|
// Find all methods that have a `Put` metadata key (`@PUT` decorator)
|
||||||
|
.filter((method) => Reflect.getMetadata('Put', controller.prototype, method))
|
||||||
|
.map((method) => {
|
||||||
|
// Get the method
|
||||||
|
const fn = controller.prototype[method];
|
||||||
|
|
||||||
|
// Get the metadata object (which contains a path and middleware)
|
||||||
|
const putRoute = Reflect.getMetadata('Put', controller.prototype, method);
|
||||||
|
const path = putRoute.path;
|
||||||
|
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loop over all the methods in the provided class looking for methods that use the `@DELETE` decorator
|
||||||
|
const pathsDELETE = Object.getOwnPropertyNames(controller.prototype)
|
||||||
|
// Find all methods that have a `Delete` metadata key (`@DELETE` decorator)
|
||||||
|
.filter((method) => Reflect.getMetadata('Delete', controller.prototype, method))
|
||||||
|
.map((method) => {
|
||||||
|
// Get the method
|
||||||
|
const fn = controller.prototype[method];
|
||||||
|
// Get the metadata object (which contains a path and middleware)
|
||||||
|
const deleteRoute = Reflect.getMetadata('Delete', controller.prototype, method);
|
||||||
|
const path = deleteRoute.path;
|
||||||
|
return path;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
GET: pathsGET,
|
GET: pathsGET,
|
||||||
POST: pathsPOST
|
POST: pathsPOST,
|
||||||
|
PUT: pathsPUT,
|
||||||
|
DELETE: pathsDELETE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async processControllerRoutes(app: Application, decoratedController: any | any[], callSetup: boolean = true) {
|
||||||
|
let addedRoutes: string[] = [];
|
||||||
|
|
||||||
|
// If the input is an array, recurse over the array
|
||||||
|
if (Array.isArray(decoratedController)) {
|
||||||
|
//console.log('Is an array - recursing...')
|
||||||
|
addedRoutes = addedRoutes.concat((await Promise.all(decoratedController.map(async (decoratedControllerCls) => await this.processControllerRoutes(app, decoratedControllerCls, callSetup)))).flat());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const controller = Reflect.getMetadata('originalClass', decoratedController);
|
||||||
|
|
||||||
|
//console.log(`Processing ${(typeof controller).toString()} - ${typeof controller !== 'undefined' && controller !== null && typeof controller.name !== 'undefined' ? controller.name : 'unknown'} (${(typeof decoratedController).toString()} - ${typeof decoratedController !== 'undefined' && decoratedController !== null && typeof decoratedController.name !== 'undefined' ? decoratedController.name : 'unknown'})...`);
|
||||||
|
|
||||||
|
let routes: { GET: string[], POST: string[], PUT: string[], DELETE: string[] } = { GET: [], POST: [], PUT: [], DELETE: [] };
|
||||||
|
try {
|
||||||
|
routes = this.getRoutesInController(controller);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(`Something went wrong while processing ${JSON.stringify(controller)} (${JSON.stringify(decoratedController)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.GET.forEach((path) => {
|
||||||
|
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.POST.forEach((path) => {
|
||||||
|
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.PUT.forEach((path) => {
|
||||||
|
addedRoutes.push(`PUT ${path} from ${(new controller()).constructor.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
routes.DELETE.forEach((path) => {
|
||||||
|
addedRoutes.push(`DELETE ${path} from ${(new controller()).constructor.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Because we reuse this method for adding information about the child routes but we don't want to call the setup method (because it's called on the parent controller)
|
||||||
|
// We have a switch to determine if we should call the setup method or not.
|
||||||
|
if (callSetup) {
|
||||||
|
decoratedController.setup(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addedRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the controllers for the application
|
* Setup the controllers for the application
|
||||||
*
|
*
|
||||||
|
|
@ -147,11 +283,14 @@ export class Router {
|
||||||
const files = await fse.readdir(this.controllersPath);
|
const files = await fse.readdir(this.controllersPath);
|
||||||
|
|
||||||
// Get the controller classes from the files
|
// Get the controller classes from the files
|
||||||
const loadedControllers = (await Promise.all(
|
const loadedControllerObjects = (await Promise.all(files.map(async (file) => (await this.checkIfControllerFile(file, this.controllersPath /*, app*/)))));
|
||||||
files.map(file => this.checkIfControllerFile(file, this.controllersPath/*, app*/))
|
|
||||||
)).filter(controller => controller !== null);
|
|
||||||
|
|
||||||
const addedRoutes: string[] = [];
|
const loadedControllers: BaseController[] = loadedControllerObjects
|
||||||
|
.map(loadedControllerObjects => loadedControllerObjects.controllers)
|
||||||
|
.filter(controller => controller !== null)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
let addedRoutes: string[] = [];
|
||||||
|
|
||||||
// Get all controllers that have the child controller decorator
|
// Get all controllers that have the child controller decorator
|
||||||
const controllersWithChildControllers = loadedControllers.filter(controller => Reflect.getMetadata('ChildController', controller) !== undefined);
|
const controllersWithChildControllers = loadedControllers.filter(controller => Reflect.getMetadata('ChildController', controller) !== undefined);
|
||||||
|
|
@ -166,69 +305,51 @@ export class Router {
|
||||||
// And forma a list that has all child controllers remove
|
// And forma a list that has all child controllers remove
|
||||||
const topLevelControllers = [...controllersWithChildControllers, ...controllersWithoutChildren];
|
const topLevelControllers = [...controllersWithChildControllers, ...controllersWithoutChildren];
|
||||||
|
|
||||||
|
//console.log(`Child controllers: ${childrenControllers.map(controller => (typeof controller).toString() + ' - ' + (typeof controller !== 'undefined' && controller !== null ? controller.name : 'unknown')).join(', ')}`);
|
||||||
|
|
||||||
// Add the routes for the child controllers to the list of added routes WITHOUT calling the setup method (as this is called in the parent controller's setup method)
|
// Add the routes for the child controllers to the list of added routes WITHOUT calling the setup method (as this is called in the parent controller's setup method)
|
||||||
childrenControllers
|
addedRoutes = addedRoutes.concat((await Promise.all(childrenControllers.map(async (decoratedController) => await this.processControllerRoutes(app, decoratedController, false)))).flat());
|
||||||
.forEach(decoratedController => {
|
|
||||||
if(Array.isArray(decoratedController)) {
|
|
||||||
decoratedController.forEach(decoratedControllerCls => {
|
|
||||||
const controller = Reflect.getMetadata('originalClass', decoratedControllerCls);
|
|
||||||
|
|
||||||
const routes = this.getRoutesInController(controller);
|
//console.log(`Top level controllers: ${topLevelControllers.map(controller => (typeof controller).toString() + ' - ' + (typeof controller !== 'undefined' && controller !== null ? controller.name : 'unknown')).join(', ')}`);
|
||||||
routes.GET.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
routes.POST.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const controller = Reflect.getMetadata('originalClass', decoratedController);
|
|
||||||
|
|
||||||
const routes = this.getRoutesInController(controller);
|
|
||||||
routes.GET.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
routes.POST.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the routes for the top level controllers to the list of added routes and call the setup method
|
// Add the routes for the top level controllers to the list of added routes and call the setup method
|
||||||
topLevelControllers
|
addedRoutes = addedRoutes.concat((await Promise.all(topLevelControllers.map(async (decoratedController) => await this.processControllerRoutes(app, decoratedController)))).flat());
|
||||||
.forEach(decoratedController => {
|
|
||||||
if(Array.isArray(decoratedController)) {
|
|
||||||
decoratedController.forEach(decoratedControllerCls => {
|
|
||||||
const controller = Reflect.getMetadata('originalClass', decoratedControllerCls);
|
|
||||||
|
|
||||||
const routes = this.getRoutesInController(controller);
|
|
||||||
routes.GET.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
routes.POST.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
decoratedControllerCls.setup(app);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const controller = Reflect.getMetadata('originalClass', decoratedController);
|
|
||||||
|
|
||||||
const routes = this.getRoutesInController(controller);
|
|
||||||
routes.GET.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`GET ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
routes.POST.forEach((path: string) => {
|
|
||||||
addedRoutes.push(`POST ${path} from ${(new controller()).constructor.name}`);
|
|
||||||
});
|
|
||||||
(decoratedController as any).setup(app);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Routes added:');
|
console.log('Routes added:');
|
||||||
addedRoutes.forEach(route => console.log('\t' + route));
|
addedRoutes.forEach(route => console.log('\t' + route));
|
||||||
|
|
||||||
|
// Adding a GET handler/middleware to handle 404 errors
|
||||||
|
// This is done by adding this as the last path (after all other routes)
|
||||||
|
app.get('*', (req, res, next) => {
|
||||||
|
// Check if the request path is in the list of outside framework routes
|
||||||
|
// This is used to allow routes outside the framework to work without getting a 404 error
|
||||||
|
if(this.outsideFrameworkRoutes.includes(req.path)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(404);
|
||||||
|
next('Page Not Found');
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadedErrorControllers = loadedControllerObjects
|
||||||
|
.map(loadedControllerObject => loadedControllerObject.errorControllers)
|
||||||
|
.filter(controller => controller !== null)
|
||||||
|
.flat()
|
||||||
|
.map(ErrorControllerClassConstructor => new ErrorControllerClassConstructor());
|
||||||
|
|
||||||
|
let handledErrors: string[] = [];
|
||||||
|
loadedErrorControllers.forEach(errorController => {
|
||||||
|
if (typeof errorController.handle === 'undefined') {
|
||||||
|
console.error(`Something went wrong while processing ${JSON.stringify(Object.entries(errorController))}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(errorController.handle.bind(errorController));
|
||||||
|
handledErrors.push(errorController.handlesError ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Error controllers added:');
|
||||||
|
handledErrors.forEach(error => console.log('\t' + error));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { BaseController } from '../controllers/BaseController';
|
import { BaseController } from '../controllers/BaseController';
|
||||||
|
|
||||||
|
export const CHILD_CONTROLLER_METADATA_KEY = 'ChildController';
|
||||||
|
|
||||||
export function ChildController<T extends { new(...args: any[]): {} }>(childController: BaseController | BaseController[]) {
|
export function ChildController<T extends { new(...args: any[]): {} }>(childController: BaseController | BaseController[]) {
|
||||||
return function (target: T) {
|
return function (target: T) {
|
||||||
// Define a metadata key for the child controller
|
// Define a metadata key for the child controller
|
||||||
Reflect.defineMetadata('ChildController', childController, target);
|
Reflect.defineMetadata(CHILD_CONTROLLER_METADATA_KEY, childController, target);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2,21 +2,23 @@ import { Application, NextFunction } from 'express';
|
||||||
|
|
||||||
import { BaseController } from '../controllers/BaseController';
|
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.
|
* 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.
|
* 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
|
* @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
|
* ```ts
|
||||||
* import { Request, Response } from 'express';
|
* import { Request, Response } from 'express';
|
||||||
*
|
*
|
||||||
* import { Controller } from '../decorators/Controller';
|
* import { Controller, GET, POST, BaseController } from '@BridgemanAccessible/ba-web-framework';
|
||||||
* import { GET } from '../decorators/GET';
|
|
||||||
* import { POST } from '../decorators/POST';
|
|
||||||
*
|
|
||||||
* import { BaseController } from './BaseController';
|
|
||||||
*
|
*
|
||||||
* @Controller()
|
* @Controller()
|
||||||
* export class MyController extends BaseController {
|
* export class MyController extends BaseController {
|
||||||
|
|
@ -25,6 +27,12 @@ import { BaseController } from '../controllers/BaseController';
|
||||||
*
|
*
|
||||||
* @POST('/path', express.json())
|
* @POST('/path', express.json())
|
||||||
* private myPostMethod(req: Request, res: Response) {}
|
* 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).
|
// 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...
|
// 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(typeof childControllers !== 'undefined') {
|
||||||
if(Array.isArray(childControllers)) {
|
if(Array.isArray(childControllers)) {
|
||||||
childControllers.forEach((childController) => {
|
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
|
// Loop over all the methods in the decorated class looking for methods that use the GET decorator
|
||||||
Object.getOwnPropertyNames(target.prototype)
|
Object.getOwnPropertyNames(target.prototype)
|
||||||
// Find all methods that have a Get metadata key (GET decorator)
|
// 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) => {
|
.map((method) => {
|
||||||
// Get the method
|
// Get the method
|
||||||
const fn = target.prototype[method];
|
const fn = target.prototype[method];
|
||||||
|
|
||||||
// Get the path
|
// 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
|
// Bind the method to the class instance
|
||||||
app.get(path, fn.bind(controller));
|
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
|
// Loop over all the methods in the decorated class looking for methods that use the POST decorator
|
||||||
Object.getOwnPropertyNames(target.prototype)
|
Object.getOwnPropertyNames(target.prototype)
|
||||||
// Find all methods that have a Post metadata key (POST decorator)
|
// 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) => {
|
.map((method) => {
|
||||||
// Get the method
|
// Get the method
|
||||||
const fn = target.prototype[method];
|
const fn = target.prototype[method];
|
||||||
|
|
||||||
// Get the metadata object (which contains a path and middleware)
|
// 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 path = postRoute.path;
|
||||||
const middleware: NextFunction[] = postRoute.middleware;
|
const middleware: NextFunction[] = postRoute.middleware;
|
||||||
|
|
||||||
// Bind the method to the class instance
|
// Bind the method to the class instance
|
||||||
app.post(path, ...middleware, fn.bind(controller));
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
* 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) {
|
export function GET(path: string) {
|
||||||
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', path, target, propertyKey);
|
Reflect.defineMetadata(GET_METADATA_KEY, path, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { NextFunction } from 'express';
|
import { NextFunction } from 'express';
|
||||||
import { NextHandleFunction } from 'connect';
|
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.
|
* 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)[]) {
|
export function POST(path: string, ...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', { path, middleware }, target, propertyKey);
|
Reflect.defineMetadata(POST_METADATA_KEY, { path, middleware }, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,50 @@
|
||||||
import { Request, Response } from 'express';
|
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[]) {
|
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) {
|
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||||
const original = descriptor.value;
|
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
|
// 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!!!
|
// !!!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
|
// 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.
|
// 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
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,11 +3,17 @@ import { ChildController } from './ChildController';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { GET } from './GET';
|
import { GET } from './GET';
|
||||||
import { POST } from './POST';
|
import { POST } from './POST';
|
||||||
|
import { PUT } from './PUT';
|
||||||
|
import { DELETE } from './DELETE';
|
||||||
|
import { ErrorHandler } from './ErrorHandler';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Controller,
|
Controller,
|
||||||
ChildController,
|
ChildController,
|
||||||
Page,
|
Page,
|
||||||
GET,
|
GET,
|
||||||
POST
|
POST,
|
||||||
}
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
ErrorHandler
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { globalTemplateValues } from './GlobalTemplateValuesMiddleware';
|
import { globalTemplateValues } from './GlobalTemplateValuesMiddleware';
|
||||||
|
import { HealthCheckStatus, HealthCheckMiddleware, healthCheckMiddleware } from './HealthCheckMiddleware';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
globalTemplateValues
|
globalTemplateValues,
|
||||||
}
|
HealthCheckStatus,
|
||||||
|
HealthCheckMiddleware,
|
||||||
|
healthCheckMiddleware
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue