UNPKG

@curveball/kernel

Version:

Curveball is a framework writting in Typescript for Node.js

187 lines (156 loc) 4.91 kB
import { EventEmitter } from 'node:events'; import { isHttpError } from '@curveball/http-errors'; import { Context } from './context.js'; import { HeadersInterface, HeadersObject } from './headers.js'; import MemoryRequest from './memory-request.js'; import MemoryResponse from './memory-response.js'; import NotFoundMw from './middleware/not-found.js'; import { Request as CurveballRequest } from './request.js'; import { Response as CurveballResponse } from './response.js'; import { curveballResponseToFetchResponse, fetchRequestToCurveballRequest } from './fetch-util.js'; import { getGlobalOrigin } from './global-origin.js'; /** * Package version * * This line gets automatically replaced during the build phase */ export const VERSION = 'Curveball/dev'; /** * The middleware-call Symbol is a special symbol that might exist as a * property on an object. * * If it exists, the object can be used as a middleware. */ const middlewareCall = Symbol('middleware-call'); export { middlewareCall }; /** * A function that can act as a middleware. */ type MiddlewareFunction = ( ctx: Context, next: () => Promise<void> ) => Promise<void> | void; type MiddlewareObject = { [middlewareCall]: MiddlewareFunction; }; export type Middleware = MiddlewareFunction | MiddlewareObject; // Calls a series of middlewares, in order. export async function invokeMiddlewares( ctx: Context, fns: Middleware[] ): Promise<void> { if (fns.length === 0) { return; } const mw: Middleware = fns[0]; let mwFunc: MiddlewareFunction; if (isMiddlewareObject(mw)) { mwFunc = mw[middlewareCall].bind(fns[0]); } else { mwFunc = mw; } return mwFunc(ctx, async () => { await invokeMiddlewares(ctx, fns.slice(1)); }); } function isMiddlewareObject(input: Middleware): input is MiddlewareObject { return (input as MiddlewareObject)[middlewareCall] !== undefined; } export default class Application extends EventEmitter { middlewares: Middleware[] = []; /** * Add a middleware to the application. * * Middlewares are called in the order they are added. */ use(...middleware: Middleware[]) { this.middlewares.push(...middleware); } /** * Handles a single request and calls all middleware. */ async handle(ctx: Context): Promise<void> { ctx.response.headers.set('Server', VERSION); ctx.response.type = 'application/hal+json'; await invokeMiddlewares(ctx, [...this.middlewares, NotFoundMw]); } /** * Executes a request on the server using the standard browser Request and * Response objects from the fetch() standard. * * Node will probably provide these out of the box in Node 18. If you're on * an older version, you'll need a polyfill. * * A use-case for this is allowing test frameworks to make fetch-like * requests without actually having to go over the network. */ async fetch(request: Request): Promise<Response> { const response = await this.subRequest( await fetchRequestToCurveballRequest(request, this.origin) ); return curveballResponseToFetchResponse(response); } /** * Does a sub-request based on a Request object, and returns a Response * object. */ async subRequest( method: string, path: string, headers?: HeadersInterface | HeadersObject, body?: any ): Promise<CurveballResponse>; async subRequest(request: CurveballRequest): Promise<CurveballResponse>; async subRequest( arg1: string | CurveballRequest, path?: string, headers?: HeadersInterface | HeadersObject, body: any = '' ): Promise<CurveballResponse> { let request: CurveballRequest; if (typeof arg1 === 'string') { request = new MemoryRequest(arg1, path!, this.origin, headers, body); } else { request = arg1; } const context = new Context(request, new MemoryResponse(this.origin)); try { await this.handle(context); } catch (err: any) { console.error(err); if (this.listenerCount('error')) { this.emit('error', err); } if (isHttpError(err)) { context.response.status = err.httpStatus; } else { context.response.status = 500; } context.response.body = 'Uncaught exception. No middleware was defined to handle it. We got the following HTTP status: ' + context.response.status; } return context.response; } private _origin?: string; /** * The public base url of the application. * * This can be auto-detected, but will often be wrong when your server is * running behind a reverse proxy or load balancer. * * To provide this, set the process.env.PUBLIC_URI property. */ get origin(): string { if (this._origin) { return this._origin; } return getGlobalOrigin(); } set origin(baseUrl: string) { this._origin = new URL(baseUrl).origin; } }