1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/main/router.ts
Janne Savolainen 4b76327489
Introduce healthy abstraction between back-end routes and route handlers
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
2022-03-11 09:20:53 +02:00

237 lines
5.2 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import Call from "@hapi/call";
import Subtext from "@hapi/subtext";
import type http from "http";
import type httpProxy from "http-proxy";
import { toPairs } from "lodash/fp";
import path from "path";
import type { Cluster } from "../common/cluster/cluster";
export interface RouterRequestOpts {
req: http.IncomingMessage;
res: http.ServerResponse;
cluster: Cluster;
params: RouteParams;
url: URL;
}
export interface RouteParams extends Record<string, string> {
path?: string; // *-route
namespace?: string;
service?: string;
account?: string;
release?: string;
repo?: string;
chart?: string;
}
export interface LensApiRequest<P = any> {
path: string;
payload: P;
params: RouteParams;
cluster: Cluster;
response: http.ServerResponse;
query: URLSearchParams;
raw: {
req: http.IncomingMessage;
res: http.ServerResponse;
};
}
const respondFor =
(contentType: string) =>
(
content: any,
statusCode: number,
response: http.ServerResponse,
) => {
response.statusCode = statusCode;
response.setHeader("Content-Type", contentType);
if (content instanceof Buffer) {
response.write(content);
response.end();
return;
}
response.end(content);
};
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
json: {
respond: (
content: any,
statusCode: number,
response: http.ServerResponse,
) => {
response.statusCode = statusCode;
response.setHeader("Content-Type", "application/json");
if (content instanceof Buffer) {
response.write(content);
response.end();
return;
}
const normalizedContent =
typeof content === "object" ? JSON.stringify(content) : content;
response.end(normalizedContent);
},
},
txt: {
respond: respondFor("text/plain"),
},
html: {
respond: respondFor("text/html"),
},
css: {
respond: respondFor("text/css"),
},
gif: {
respond: respondFor("image/gif"),
},
jpg: {
respond: respondFor("image/jpeg"),
},
png: {
respond: respondFor("image/png"),
},
svg: {
respond: respondFor("image/svg+xml"),
},
js: {
respond: respondFor("application/javascript"),
},
woff2: {
respond: respondFor("font/woff2"),
},
ttf: {
respond: respondFor("font/ttf"),
},
};
export class Router {
protected router = new Call.Router();
static rootPath = path.resolve(__static);
constructor(routes: Route[]) {
routes.forEach(route => {
this.router.add({ method: route.method, path: route.path }, async (request: LensApiRequest) => {
let result: LensApiResult | void;
try {
result = await route.handler(request);
} catch(error) {
contentTypes.txt.respond(error.toString(), 422, request.response);
return;
}
if (!result) {
contentTypes.txt.respond(null, 204, request.response);
return;
}
const {
response,
error,
statusCode = error ? 400 : 200,
contentType = contentTypes.json,
headers = {},
proxy,
} = result;
if (proxy) {
return;
}
const headerNameValuePairs = toPairs<string>(headers);
headerNameValuePairs.forEach(([key, value]) => {
request.response.setHeader(key, value);
});
contentType.respond(error || response, statusCode, request.response);
});
});
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const url = new URL(req.url, "http://localhost");
const path = url.pathname;
const method = req.method.toLowerCase();
const matchingRoute = this.router.route(method, path);
const routeFound = !matchingRoute.isBoom;
if (routeFound) {
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params });
await matchingRoute.route(request);
return true;
}
return false;
}
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
const { req, res, url, cluster, params } = opts;
const { payload } = await Subtext.parse(req, null, {
parse: true,
output: "data",
});
return {
cluster,
path: url.pathname,
raw: {
req, res,
},
response: res,
query: url.searchParams,
payload,
params,
};
}
}
interface LensApiResultContentType {
respond: (content: any, statusCode: number, response: http.ServerResponse) => void;
}
export interface LensApiResult {
statusCode?: number;
response?: any;
error?: any;
contentType?: LensApiResultContentType;
headers?: { Location: string };
proxy?: httpProxy;
}
export interface Route {
path: string;
method: "get" | "post" | "put" | "patch" | "delete";
handler: (request: LensApiRequest) => LensApiResult | Promise<LensApiResult>;
}