UNPKG

@nahkies/typescript-express-runtime

Version:

Runtime package for code generated by @nahkies/openapi-code-generator using the typescript-express template

251 lines (212 loc) 6.24 kB
import type {Server} from "node:http" import type {AddressInfo, ListenOptions} from "node:net" import { type Res, SkipResponse, type StatusCode, } from "@nahkies/typescript-common-runtime/types" import type {OptionsJson, OptionsText, OptionsUrlencoded} from "body-parser" import Cors, {type CorsOptions, type CorsOptionsDelegate} from "cors" import type {Response} from "express" import express, { type ErrorRequestHandler, type Express, type Response as ExpressResponse, type Request, type RequestHandler, type Router, } from "express" import {ExpressRuntimeError} from "./errors.ts" export {parseQueryParameters} from "@nahkies/typescript-common-runtime/query-parser" import { parseOctetStreamRequestBody, type SizeLimit, } from "@nahkies/typescript-common-runtime/request-bodies/octet-stream" export { type Params, type Res, SkipResponse, type StatusCode, type StatusCode1xx, type StatusCode2xx, type StatusCode3xx, type StatusCode4xx, type StatusCode5xx, } from "@nahkies/typescript-common-runtime/types" // biome-ignore lint/suspicious/noExplicitAny: needed export type ResponseValidator = (status: number, value: unknown) => any export class ExpressRuntimeResponse<Type> { private _body?: Type constructor(private readonly status: StatusCode) {} body(body: Type): this { this._body = body return this } unpack(): Res<StatusCode, Type | undefined> { return {status: this.status, body: this._body} } } export function handleResponse( res: Response, validator: (status: number, value: unknown) => unknown, ) { return async ( response: | ExpressRuntimeResponse<unknown> | typeof SkipResponse | Res<StatusCode, unknown>, ): Promise<void> => { // escape hatch to allow responses to be sent by the implementation handler if (response === SkipResponse) { return } const {status, body} = response instanceof ExpressRuntimeResponse ? response.unpack() : response res.status(status) if (body === undefined) { res.end() return } if (body instanceof Blob) { await sendBlob(res, body) } else { res.json(validator(status, body)) } } } export function handleImplementationError(err: unknown): never { throw ExpressRuntimeError.HandlerError(err) } export type ExpressRuntimeResponder< Status extends StatusCode = StatusCode, // biome-ignore lint/suspicious/noExplicitAny: needed Type = any, > = { withStatus: (status: Status) => ExpressRuntimeResponse<Type> } export type ServerConfig = { /** * set to "disabled" to disable cors middleware, omit or pass undefined for defaults * * by default, all origins are allowed. you probably don't want this in production, * so it's strongly recommended to explicitly configure this. **/ cors?: "disabled" | CorsOptions | CorsOptionsDelegate | undefined /** * set to "disabled" to disable body parsing middleware, omit or pass undefined for defaults. * * if disabling, ensure you pass a body parsing middleware that places the parsed * body on `req.body` for request body processing to work. **/ body?: | "disabled" | Partial<{ json: OptionsJson text: OptionsText urlencoded: OptionsUrlencoded }> | undefined /** * provide arbitrary express middleware to be mounted before all request handlers * useful for mounting logging, alternative body parsers, etc */ middleware?: RequestHandler[] /** * Provide a custom 404 handler */ notFoundHandler?: RequestHandler /** * Provide a custom error handler */ errorHandler?: ErrorRequestHandler /** * the router to use, normally obtained by calling the generated `createRouter` * function */ router: Router /** * the port to listen on, a randomly allocated port will be used if none passed * alternatively ListenOptions can be passed to control the network interface * bound to. */ port?: number | ListenOptions } /** * Starts an Express server and listens on `port` or a randomly allocated port if none provided. * Enables CORS and body parsing by default. It's recommended to customize the CORS options * for production usage. * * If you need more control over your Express server you should avoid calling this function, * and instead mount the router from your generated codes `createRouter` call directly * onto a server you have constructed. */ export async function startServer({ middleware = [], cors = undefined, body = undefined, port = 0, router, notFoundHandler, errorHandler, }: ServerConfig): Promise<{ app: Express server: Server address: AddressInfo }> { const app = express() if (cors !== "disabled") { app.use(Cors(cors)) app.options("*route", Cors(cors)) } if (body !== "disabled") { app.use(express.json(body?.json)) app.use(express.text(body?.text)) app.use(express.urlencoded(body?.urlencoded ?? {extended: true})) } if (middleware) { for (const it of middleware) { app.use(it) } } app.use(router) if (notFoundHandler) { app.use(notFoundHandler) } if (errorHandler) { app.use(errorHandler) } return new Promise((resolve, reject) => { try { const server = app.listen(port) server.once("listening", () => { try { const address = server.address() if (!address || typeof address !== "object") { throw new Error("failed to bind port") } resolve({app, server, address}) } catch (err) { reject(err) } }) server.once("error", (err) => { reject(err) }) } catch (err) { reject(err) } }) } export async function parseOctetStream( req: Request, sizeLimit: SizeLimit, ): Promise<Blob | undefined> { return parseOctetStreamRequestBody(req, {sizeLimit}) } export async function sendBlob(res: ExpressResponse, body: Blob) { const arrayBuffer = await body.arrayBuffer() const buffer = Buffer.from(arrayBuffer) res.setHeader("Content-Type", body.type ?? "application/octet-stream") res.setHeader("Content-Length", buffer.length) res.send(buffer) }