Refactor replace some HTTP related magic values by enums.

This commit is contained in:
baldo 2022-09-20 19:09:49 +02:00
parent c988227bc7
commit 15d3f45bae
9 changed files with 135 additions and 24 deletions

View file

@ -11,6 +11,7 @@ import { config } from "./config";
import type { CleartextPassword, PasswordHash, Username } from "./types"; import type { CleartextPassword, PasswordHash, Username } from "./types";
import { isString } from "./types"; import { isString } from "./types";
import Logger from "./logger"; import Logger from "./logger";
import { HttpHeader, HttpStatusCode, MimeType } from "./shared/utils/http";
export const app: Express = express(); export const app: Express = express();
@ -111,7 +112,7 @@ export function init(): void {
router.use(compress()); router.use(compress());
async function serveTemplate( async function serveTemplate(
mimeType: string, mimeType: MimeType,
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
@ -120,13 +121,15 @@ export function init(): void {
"utf8" "utf8"
); );
res.writeHead(200, { "Content-Type": mimeType }); res.writeHead(HttpStatusCode.OK, {
[HttpHeader.CONTENT_TYPE]: mimeType,
});
res.end(_.template(body)({ config: config.client })); res.end(_.template(body)({ config: config.client }));
} }
usePromise(async (req: Request, res: Response): Promise<void> => { usePromise(async (req: Request, res: Response): Promise<void> => {
if (jsTemplateFiles.indexOf(req.path) >= 0) { if (jsTemplateFiles.indexOf(req.path) >= 0) {
await serveTemplate("application/javascript", req, res); await serveTemplate(MimeType.APPLICATION_JSON, req, res);
} }
}); });

View file

@ -7,6 +7,7 @@ import { normalizeString, parseInteger } from "../shared/utils/strings";
import { forConstraint } from "../shared/validation/validator"; import { forConstraint } from "../shared/validation/validator";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { isString, Mail, MailId } from "../types"; import { isString, Mail, MailId } from "../types";
import { HttpHeader } from "../shared/utils/http";
const isValidId = forConstraint(CONSTRAINTS.id, false); const isValidId = forConstraint(CONSTRAINTS.id, false);
@ -39,7 +40,7 @@ async function doGetAll(
export function getAll(req: Request, res: Response): void { export function getAll(req: Request, res: Response): void {
doGetAll(req) doGetAll(req)
.then(({ total, mails }) => { .then(({ total, mails }) => {
res.set("X-Total-Count", total.toString(10)); res.set(HttpHeader.X_TOTAL_COUNT, total.toString(10));
return Resources.success(res, mails); return Resources.success(res, mails);
}) })
.catch((err) => Resources.error(res, err)); .catch((err) => Resources.error(res, err));

View file

@ -14,6 +14,7 @@ import {
NodeMonitoringStateResponse, NodeMonitoringStateResponse,
toMonitoringResponse, toMonitoringResponse,
} from "../types"; } from "../types";
import { HttpHeader } from "../shared/utils/http";
const isValidToken = forConstraint(CONSTRAINTS.token, false); const isValidToken = forConstraint(CONSTRAINTS.token, false);
@ -33,7 +34,7 @@ async function doGetAll(
export function getAll(req: Request, res: Response): void { export function getAll(req: Request, res: Response): void {
doGetAll(req) doGetAll(req)
.then(({ total, result }) => { .then(({ total, result }) => {
res.set("X-Total-Count", total.toString(10)); res.set(HttpHeader.X_TOTAL_COUNT, total.toString(10));
Resources.success(res, result); Resources.success(res, result);
}) })
.catch((err) => Resources.error(res, err)); .catch((err) => Resources.error(res, err));

View file

@ -27,6 +27,7 @@ import {
toNodeTokenResponse, toNodeTokenResponse,
} from "../types"; } from "../types";
import { filterUndefinedFromJSON } from "../shared/utils/json"; import { filterUndefinedFromJSON } from "../shared/utils/json";
import { HttpHeader } from "../shared/utils/http";
const nodeFields = [ const nodeFields = [
"hostname", "hostname",
@ -165,7 +166,7 @@ export function getAll(req: Request, res: Response): void {
total: number; total: number;
pageNodes: DomainSpecificNodeResponse[]; pageNodes: DomainSpecificNodeResponse[];
}) => { }) => {
res.set("X-Total-Count", result.total.toString(10)); res.set(HttpHeader.X_TOTAL_COUNT, result.total.toString(10));
return Resources.success( return Resources.success(
res, res,
result.pageNodes.map(filterUndefinedFromJSON) result.pageNodes.map(filterUndefinedFromJSON)

View file

@ -14,6 +14,7 @@ import {
TaskState, TaskState,
UnixTimestampSeconds, UnixTimestampSeconds,
} from "../types"; } from "../types";
import { HttpHeader } from "../shared/utils/http";
const isValidId = forConstraint(CONSTRAINTS.id, false); const isValidId = forConstraint(CONSTRAINTS.id, false);
@ -109,7 +110,7 @@ async function doGetAll(
export function getAll(req: Request, res: Response): void { export function getAll(req: Request, res: Response): void {
doGetAll(req) doGetAll(req)
.then(({ total, pageTasks }) => { .then(({ total, pageTasks }) => {
res.set("X-Total-Count", total.toString(10)); res.set(HttpHeader.X_TOTAL_COUNT, total.toString(10));
Resources.success(res, pageTasks.map(toTaskResponse)); Resources.success(res, pageTasks.map(toTaskResponse));
}) })
.catch((err) => Resources.error(res, err)); .catch((err) => Resources.error(res, err));

View file

@ -68,6 +68,18 @@ export type Url = string & { readonly __tag: unique symbol };
*/ */
export const isUrl = toIsNewtype(isString, "" as Url); export const isUrl = toIsNewtype(isString, "" as Url);
/**
* Typesafe string representation of paths.
*/
export type Path = string & { readonly __tag: unique symbol };
/**
* Type guard for {@link Path}.
*
* @param arg - Value to check.
*/
export const isPath = toIsNewtype(isString, "" as Url);
/** /**
* Fastd VPN key of a Freifunk node. This is the key used by the node to open a VPN tunnel to Freifunk gateways. * Fastd VPN key of a Freifunk node. This is the key used by the node to open a VPN tunnel to Freifunk gateways.
*/ */

View file

@ -0,0 +1,84 @@
/**
* Enum representing supported HTTP methods.
*/
export enum HttpMethod {
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET}.
*/
GET = "GET",
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST}.
*/
POST = "POST",
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT}.
*/
PUT = "PUT",
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE}.
*/
DELETE = "DELETE",
}
/**
* Enum representing supported HTTP headers.
*/
export enum HttpHeader {
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type}.
*/
CONTENT_TYPE = "Content-Type",
/**
* Holds the total number of entities known by the server matching the request (ignoring paging parameters).
*/
X_TOTAL_COUNT = "X-Total-Count",
}
/**
* Enum representing supported mime-types.
*/
export enum MimeType {
/**
* The content is JSON.
*/
APPLICATION_JSON = "application/json",
/**
* The content is (X)HTML.
*/
TEXT_HTML = "text/html",
}
/**
* Enum representing supported HTTP response status codes.
*/
export enum HttpStatusCode {
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200}.
*/
OK = 200,
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400}.
*/
BAD_REQUEST = 400,
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404}.
*/
NOT_FOUND = 404,
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409}.
*/
CONFLICT = 409,
/**
* See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500}.
*/
INTERNAL_SERVER_ERROR = 500,
}

View file

@ -1,6 +1,9 @@
import { HttpStatusCode } from "../shared/utils/http";
// TODO: Replace this by throwing typed errors.
export default { export default {
badRequest: { code: 400 }, badRequest: { code: HttpStatusCode.BAD_REQUEST },
notFound: { code: 404 }, notFound: { code: HttpStatusCode.NOT_FOUND },
conflict: { code: 409 }, conflict: { code: HttpStatusCode.CONFLICT },
internalError: { code: 500 }, internalError: { code: HttpStatusCode.INTERNAL_SERVER_ERROR },
}; };

View file

@ -22,6 +22,7 @@ import {
TypeGuard, TypeGuard,
} from "../types"; } from "../types";
import { getFieldIfExists } from "../shared/utils/objects"; import { getFieldIfExists } from "../shared/utils/objects";
import { HttpHeader, HttpStatusCode, MimeType } from "../shared/utils/http";
export type RequestData = JSONObject; export type RequestData = JSONObject;
export type RequestHandler = (request: Request, response: Response) => void; export type RequestHandler = (request: Request, response: Response) => void;
@ -46,30 +47,34 @@ export type FilterClause = { query: string; params: unknown[] };
function respond( function respond(
res: Response, res: Response,
httpCode: number, httpCode: HttpStatusCode,
data: string, data: string,
type: "html" type: MimeType.TEXT_HTML
): void; ): void;
function respond( function respond(
res: Response, res: Response,
httpCode: number, httpCode: HttpStatusCode,
data: JSONValue, data: JSONValue,
type: "json" type: MimeType.APPLICATION_JSON
): void; ): void;
function respond( function respond(
res: Response, res: Response,
httpCode: number, httpCode: HttpStatusCode,
data: JSONValue, data: JSONValue,
type: "html" | "json" type: MimeType.APPLICATION_JSON | MimeType.TEXT_HTML
): void { ): void {
switch (type) { switch (type) {
case "html": case MimeType.TEXT_HTML:
res.writeHead(httpCode, { "Content-Type": "text/html" }); res.writeHead(httpCode, {
[HttpHeader.CONTENT_TYPE]: MimeType.TEXT_HTML,
});
res.end(data); res.end(data);
break; break;
default: default:
res.writeHead(httpCode, { "Content-Type": "application/json" }); res.writeHead(httpCode, {
[HttpHeader.CONTENT_TYPE]: MimeType.APPLICATION_JSON,
});
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
break; break;
} }
@ -365,18 +370,18 @@ export function filterClause<SortField>(
} }
export function success(res: Response, data: JSONValue) { export function success(res: Response, data: JSONValue) {
respond(res, 200, data, "json"); respond(res, HttpStatusCode.OK, data, MimeType.APPLICATION_JSON);
} }
export function successHtml(res: Response, html: string) { export function successHtml(res: Response, html: string) {
respond(res, 200, html, "html"); respond(res, HttpStatusCode.OK, html, MimeType.APPLICATION_JSON);
} }
export function error( export function error(
res: Response, res: Response,
err: { data: JSONValue; type: { code: number } } err: { data: JSONValue; type: { code: HttpStatusCode } }
) { ) {
respond(res, err.type.code, err.data, "json"); respond(res, err.type.code, err.data, MimeType.APPLICATION_JSON);
} }
export function handleJSON<Response>( export function handleJSON<Response>(