@curveball/kernel
Version:
Curveball is a framework writting in Typescript for Node.js
187 lines (156 loc) • 4.91 kB
text/typescript
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;
}
}