Files
openstapps/src/routes/Route.ts
2021-04-27 13:01:25 +02:00

149 lines
5.3 KiB
TypeScript

/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
SCInternalServerErrorResponse,
SCMethodNotAllowedErrorResponse,
SCRoute,
SCValidationErrorResponse,
} from '@openstapps/core';
import {Application, Router} from 'express';
import PromiseRouter from 'express-promise-router';
import {ValidationError} from 'jsonschema';
import {isTestEnvironment, logger, validator} from '../common';
import {isHttpMethod} from './HTTPTypes';
/**
* Creates a router from a route class (model of a route) and a handler function which implements the logic
*
* The given router performs a request and respone validation, sets status codes and checks if the given handler
* only returns errors that are allowed for the client to see
*
* @param routeClass
* @param handler
*/
export function createRoute<RETURNTYPE>(
routeClass: SCRoute,
handler: (validatedBody: any, app: Application, params?: { [parameterName: string]: string }) => Promise<RETURNTYPE>,
): Router {
// create router
const router = PromiseRouter({mergeParams: true});
// create route
// the given type has no index signature so we have to cast to get the IRouteHandler when a HTTP method is given
const route = router.route(routeClass.urlFragment);
// get route parameters (path parameters)
if (Array.isArray(routeClass.obligatoryParameters) && routeClass.obligatoryParameters.length > 0) {
routeClass.obligatoryParameters.forEach((parameterName) => {
router.param(parameterName, async (req, _res, next, parameterValue: string) => {
if (typeof req.params === 'undefined') {
req.params = {};
}
// set parameter values on request object
req.params[parameterName] = parameterValue;
// hand over the request to the next handler (our method route handler)
next();
});
});
}
const verb = routeClass.method.toLowerCase();
// check if route has a valid http verb
if (isHttpMethod(verb)) {
// create a route handler for the given HTTP method
route[verb](async (req, res) => {
try {
// validate request
const requestValidation = validator.validate(req.body, routeClass.requestBodyName);
if (requestValidation.errors.length > 0) {
const error = new SCValidationErrorResponse(
requestValidation.errors,
isTestEnvironment,
);
res.status(error.statusCode);
res.json(error);
logger.warn(error);
return;
}
// hand over request to handler with path parameters
const response = await handler(req.body, req.app, req.params);
// validate response generated by handler
const responseErrors: ValidationError[] = validator.validate(response, routeClass.responseBodyName).errors;
if (responseErrors.length > 0) {
const validationError = new SCValidationErrorResponse(
responseErrors,
isTestEnvironment,
);
// The validation error is not caused by faulty user input, but through an error that originates somewhere in
// the backend, therefor we use this "stacked" error.
const internalServerError = new SCInternalServerErrorResponse(
validationError,
isTestEnvironment,
);
res.status(internalServerError.statusCode);
res.json(internalServerError);
logger.warn(internalServerError);
return;
}
// set status code
res.status(routeClass.statusCodeSuccess);
// respond
res.json(response);
} catch (error) {
// if the error response is allowed on the route
if (routeClass.errorNames.some((constructorType) => error instanceof constructorType)) {
// respond with the error from the handler
res.status(error.statusCode);
res.json(error);
logger.warn(error);
} else {
// the error is not allowed so something went wrong
const internalServerError = new SCInternalServerErrorResponse(
error,
isTestEnvironment,
);
res.status(internalServerError.statusCode);
res.json(internalServerError);
logger.error(error);
}
}
});
} else {
throw new Error('Invalid HTTP verb in route definition. Please check route definitions in `@openstapps/core`');
}
// return a SCMethodNotAllowedErrorResponse on all other HTTP methods
route.all((_req, res) => {
const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment);
res.status(error.statusCode);
res.json(error);
logger.warn(error);
});
// return router
return router;
}