@intuitionrobotics/thunderstorm
Version:
322 lines • 12.7 kB
JavaScript
import { _keys, BadImplementationException, dispatch_onServerError, isErrorOfType, Logger, LogLevel, merge, MUSTNeverHappenException, ServerErrorSeverity, validate, ValidationException } from "@intuitionrobotics/ts-common";
import { Stream } from "stream";
import { parse } from "url";
import {} from "./HttpServer.js";
import {} from "http";
// noinspection TypeScriptPreferShortImport
import { HttpMethod } from "../../../shared/types.js";
import { assertProperty } from "../../utils/to-be-removed.js";
import { ApiException } from "../../exceptions.js";
import {} from "../../utils/types.js";
import { RemoteProxy } from "../proxy/RemoteProxy.js";
import { Storm } from "../../core/Storm.js";
export class ServerApi extends Logger {
static isDebug;
printResponse = true;
headersToLog = [];
method;
// The api's own URL fragment, used by dynamic-endpoint-array sites
// (e.g. DB API generator output) that iterate a list and mount each
// at its own path. For static endpoints declared in the routes file,
// leave this empty — the routes file owns the path.
relativePath;
middlewares;
bodyValidator;
queryValidator;
scopes = [];
constructor(method, relativePath = "", tag) {
super(tag || relativePath || method);
this.setMinLevel(ServerApi.isDebug ? LogLevel.Verbose : LogLevel.Info);
this.method = method;
this.relativePath = `${relativePath}`;
}
// The Express RequestHandler form of this endpoint. Pass it straight to
// your routes file: `v1.post("/register", register.handler)`. Stored on
// the instance after first access so repeated reads are O(1).
_handler;
get handler() {
if (!this._handler)
this._handler = (req, res) => this.call(req, res);
return this._handler;
}
shouldPrintResponse() {
return this.printResponse;
}
;
setMiddlewares(...middlewares) {
this.middlewares = middlewares;
return this;
}
addMiddlewares(...middlewares) {
this.middlewares = [...(this.middlewares || []), ...middlewares];
return this;
}
setScopes(...scopes) {
this.scopes = [...scopes];
return this;
}
getScopes() {
return this.scopes;
}
addHeaderToLog(...headersToLog) {
this.headersToLog.push(...headersToLog);
}
setBodyValidator(bodyValidator) {
this.bodyValidator = bodyValidator;
}
setQueryValidator(queryValidator) {
this.queryValidator = queryValidator;
}
asProxy() {
return new ServerApi_Proxy(this);
}
dontPrintResponse() {
this.printResponse = false;
}
setMaxResponsePrintSize(printResponseMaxSizeBytes) {
this.printResponse = printResponseMaxSizeBytes > -1;
}
assertProperty = assertProperty;
async call(req, res) {
const response = new ApiResponse(this, res);
this.logInfo(`Intercepted Url: ${req.path}`);
if (this.headersToLog.length > 0) {
const headers = {};
for (const headerName of this.headersToLog) {
headers[headerName] = req.header(headerName);
}
this.logDebug(`-- Headers: `, headers);
}
const reqQuery = parse(req.url, true).query;
if (reqQuery && typeof reqQuery === "object" && Object.keys(reqQuery).length)
this.logVerbose(`-- Url Params: `, reqQuery);
else
this.logVerbose(`-- No Params`);
const body = req.body;
if (body && ((typeof body === "object")))
this.logVerbose(`-- Body (Object): `, body);
else if (body && body.length)
this.logVerbose(`-- Body (String): `, body);
else
this.logVerbose(`-- No Body`);
const requestData = {
method: this.method,
originalUrl: req.path,
headers: req.headers,
url: req.url,
query: reqQuery,
body: body
};
try {
if (this.bodyValidator)
validate(body, this.bodyValidator);
if (this.queryValidator)
validate(reqQuery, this.queryValidator);
const context = await this.applyMiddlewares(req, requestData, response, this.scopes);
const toReturn = await this.process(req, response, reqQuery, body, context);
if (response.isConsumed())
return;
if (!toReturn)
return await response.end(200);
// TODO need to handle stream and buffers
// if (Buffer.isBuffer(toReturn))
// return response.stream(200, toReturn as Buffer);
const responseType = typeof toReturn;
if (responseType === "object")
return await response.json(200, toReturn);
if (responseType === "string" && toReturn.toLowerCase().startsWith("<html"))
return await response.html(200, toReturn);
return await response.text(200, toReturn);
}
catch (err) {
let dispatchError = true;
let e = err;
let severity = ServerErrorSeverity.Warning;
if (typeof e === "string")
e = new BadImplementationException(`String was thrown: ${e}`);
if (!(e instanceof Error) && typeof e === "object")
e = new BadImplementationException(`Object instance was thrown: ${JSON.stringify(e)}`);
try {
this.logErrorBold(e);
}
catch (e2) {
this.logErrorBold("Error while handling error on request...", e2);
this.logErrorBold(`Original error thrown: ${JSON.stringify(e)}`);
this.logErrorBold(`-- Someone was stupid... you MUST only throw an Error and not objects or strings!! --`);
}
if (isErrorOfType(e, ValidationException))
e = new ApiException(400, "Validator exception", e);
if (!isErrorOfType(e, ApiException))
e = new ApiException(500, "Unexpected server error", e);
const apiException = isErrorOfType(e, ApiException);
if (!apiException)
throw new MUSTNeverHappenException("MUST NEVER REACH HERE!!!");
dispatchError = apiException.getDispatchError();
if (apiException.responseCode >= 500)
severity = ServerErrorSeverity.Error;
else if (apiException.responseCode >= 400)
severity = ServerErrorSeverity.Warning;
switch (apiException.responseCode) {
case 401:
severity = ServerErrorSeverity.Debug;
break;
case 404:
severity = ServerErrorSeverity.Info;
break;
case 403:
severity = ServerErrorSeverity.Warning;
break;
case 500:
severity = ServerErrorSeverity.Critical;
break;
}
if (dispatchError) {
try {
const storm = Storm.getInstance();
if (storm) {
const message = await storm.errorMessageComposer(requestData, apiException);
await dispatch_onServerError.dispatchModuleAsync(severity, message);
}
}
catch (e) {
this.logError("Error while handing server error", e);
}
}
if (apiException.responseCode === 500)
return response.serverError(apiException);
return response.exception(apiException);
}
}
async applyMiddlewares(req, requestData, response, scopes) {
if (!this.middlewares)
return {};
const contextList = await Promise.all(this.middlewares.map(async (m) => m(req, requestData, response, scopes)));
return contextList.reduce((acc, c) => merge(acc, c || {}), {});
}
}
// ServerApi_Get / ServerApi_Post were dropped — every endpoint now extends
// ServerApi<Binder> directly and passes its HttpMethod to super().
export class ServerApi_Proxy extends ServerApi {
api;
constructor(api) {
super(api.method, `${api.relativePath}/proxy`);
this.api = api;
this.setMiddlewares(RemoteProxy.Middleware);
}
async process(request, response, queryParams, body, context) {
// @ts-expect-error TS struggles with this dynamic typing
return this.api.process(request, response, queryParams, body, context);
}
}
export class ServerApi_Redirect extends ServerApi {
responseCode;
redirectUrl;
constructor(apiName, responseCode, redirectUrl) {
super(HttpMethod.ALL, apiName);
this.responseCode = responseCode;
this.redirectUrl = redirectUrl;
}
async process(request, response, queryParams, _body) {
const query = queryParams ? _keys(queryParams).reduce((c, k) => c + '&' + k + '=' + queryParams[k], '?') : '';
// v2.0: `redirectUrl` must be either absolute or Express-relative.
// Pre-v2 the framework prefixed it with HttpServer.config.baseUrl —
// callers that relied on that should now pass the full URL themselves.
response.redirect(this.responseCode, `${this.redirectUrl}${query}`);
}
}
export class ApiResponse {
api;
res;
consumed = false;
constructor(api, res) {
this.api = api;
this.res = res;
}
isConsumed() {
return this.consumed;
}
consume() {
if (this.consumed) {
this.api.logError("This API was already satisfied!!", new Error());
return;
}
this.consumed = true;
}
stream(responseCode, stream, headers) {
this.consume();
this.printHeaders(headers);
this.res.set(headers);
this.res.writeHead(responseCode);
stream.pipe(this.res, { end: false });
stream.on('end', () => {
this.res.end();
});
}
printHeaders(headers) {
if (!headers)
return this.api.logVerbose(` -- No response headers`);
this.api.logVerbose(` -- Response with headers: `, headers);
}
printResponse(response) {
if (!response)
return this.api.logVerbose(` -- No response body`);
if (!this.api.shouldPrintResponse())
return this.api.logVerbose(` -- Response: -- Not Printing --`);
this.api.logVerbose(` -- Response:`, response);
}
code(responseCode, headers) {
this.printHeaders(headers);
this.end(responseCode, "", headers);
}
text(responseCode, response, _headers) {
const headers = (_headers || {});
headers["content-type"] = "text/plain";
this.end(responseCode, response, headers);
}
html(responseCode, response, _headers) {
const headers = (_headers || {});
headers["content-type"] = "text/html";
this.end(responseCode, response, headers);
}
json(responseCode, response, _headers) {
this._json(responseCode, response, _headers);
}
_json(responseCode, response, _headers) {
const headers = (_headers || {});
headers["content-type"] = "application/json";
this.end(responseCode, response, headers);
}
end(responseCode, response, headers) {
this.consume();
this.printHeaders(headers);
this.printResponse(response);
this.res.set(headers);
this.res.writeHead(responseCode);
this.res.end(typeof response !== "string" ? JSON.stringify(response, null, 2) : response);
}
setHeaders(headers) {
this.res.header(headers);
}
setHeader(headerKey, value) {
this.res.header(headerKey, value);
}
getHeader(headerKey) {
return this.res.get(headerKey);
}
redirect(responseCode, url) {
this.consume();
this.res.redirect(responseCode, url);
}
exception(exception, headers) {
const responseBody = exception.responseBody;
if (!ServerApi.isDebug)
delete responseBody.debugMessage;
this._json(exception.responseCode, responseBody, headers);
}
serverError(error, headers) {
const stack = error.cause ? error.cause.stack : error.stack;
const message = (error.cause ? error.cause.message : error.message) || "";
this.text(500, ServerApi.isDebug && stack ? stack : message, headers);
}
}
//# sourceMappingURL=server-api.js.map