marmar-context / web / context / test / fixture / http.ts
http.ts
Raw
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Session, Store } from "./store";

export class HttpError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string,
    readonly details?: unknown,
  ) {
    super(message);
  }
}

export type Ctx = {
  req: IncomingMessage;
  res: ServerResponse;
  url: URL;
  params: string[];
  // Request bodies are checked field by field in each handler.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  body: any;
  session: Session | null;
  store: Store;
};
export type Route = {
  method: string;
  pattern: RegExp;
  /** public: anyone; session: signed in with an email code; step: also passed the second factor; patients: also accepted the terms (as requireClinician({ realPatients })). */
  auth: "public" | "session" | "step" | "patients";
  handler: (ctx: Ctx) => unknown;
  /** The backend answers 201 for creates and 202 for an access request. */
  status: number;
};

export const route = (method: string, path: string, auth: Route["auth"], handler: Route["handler"], status = 200): Route => ({
  method,
  pattern: new RegExp(`^${path.replace(/:[a-zA-Z]+/g, "([^/]+)")}$`),
  auth,
  handler,
  status,
});

export const text = (value: unknown, max = 500) => (typeof value === "string" ? value.trim().slice(0, max) : "");
export const need = <T>(value: T | undefined | null, what: string): T => {
  if (value === undefined || value === null) throw new HttpError(404, "NOT_FOUND", `${what} was not found.`);
  return value;
};
export const invalid = (message: string, details?: unknown) => new HttpError(400, "VALIDATION_ERROR", message, details);
export const clinicianName = (store: Store) => store.clinician.fullName;