@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
text/typescript
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)
}