mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-19 16:13:06 +00:00
Also: - Add functionality for serving the responses from plugins - Add tests for related methods and routes Closes #2, #37
154 lines
5.3 KiB
TypeScript
154 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 {Logger} from '@openstapps/logger';
|
|
import {Application, Router} from 'express';
|
|
import PromiseRouter from 'express-promise-router';
|
|
import {ValidationError} from 'jsonschema';
|
|
import {isTestEnvironment, validator} from '../common';
|
|
import {isHttpMethod} from './http-types';
|
|
|
|
/**
|
|
* Creates a router from a route class 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 Model of a route
|
|
* @param handler Implements the logic of the route
|
|
*/
|
|
export function createRoute<REQUESTTYPE, RETURNTYPE>(
|
|
routeClass: SCRoute,
|
|
handler: (
|
|
validatedBody: REQUESTTYPE,
|
|
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);
|
|
await 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;
|
|
}
|