Template
1
0
Files
boilerplate/libraries/api.ts
2025-04-21 00:18:46 +00:00

170 lines
4.7 KiB
TypeScript

import z from "zod";
import { BadRequestError, InternalServerError, NotFoundError, RelayError } from "./errors.ts";
import { Procedure } from "./procedure.ts";
export class RelayApi<TProcedures extends Procedure[]> {
/**
* Route index in the '${method} ${path}' format allowing for quick access to
* a specific route.
*/
readonly #index = new Map<string, Procedure>();
/**
* Instantiate a new Server instance.
*
* @param routes - Routes to register with the instance.
*/
constructor({ procedures }: Config<TProcedures>) {
for (const procedure of procedures) {
this.#index.set(procedure.method, procedure);
}
}
/**
* Handle a incoming fetch request.
*
* @param method - Method name being executed.
* @param params - Parameters provided with the method request.
* @param id - Request id used for response identification.
*/
async call(method: string, params: unknown, id: string): Promise<Response> {
const procedure = this.#index.get(method);
if (procedure === undefined) {
return toResponse(new NotFoundError(`Method '' does not exist`), id);
}
// ### Context
// Context is passed to every route handler and provides a suite of functionality
// and request data.
const args: any[] = [];
// ### Params
// If the route has a body schema we need to validate and parse the body.
if (procedure.state.params !== undefined) {
if (params === undefined) {
return toResponse(new BadRequestError("Procedure expected 'params' but got 'undefined'."), id);
}
const result = await procedure.state.params.safeParseAsync(params);
if (result.success === false) {
return toResponse(new BadRequestError("Invalid request body", z.prettifyError(result.error)), id);
}
args.push(result.data);
}
// ### Actions
// Run through all assigned actions for the route.
const data: Record<string, unknown> = {};
if (procedure.state.actions !== undefined) {
for (const entry of procedure.state.actions) {
let action = entry;
let input: any;
if (Array.isArray(entry)) {
action = entry[0];
input = entry[1](args[0]);
}
const result = (await action.state.input?.safeParseAsync(input)) ?? { success: true, data: {} };
if (result.success === false) {
return toResponse(new BadRequestError("Invalid action input", z.prettifyError(result.error)), id);
}
if (action.state.handle === undefined) {
return toResponse(new InternalServerError(`Action '${action.state.name}' is missing handler.`), id);
}
const output = await action.state.handle(result.data);
if (output instanceof RelayError) {
return toResponse(output, id);
}
for (const key in output) {
data[key] = output[key];
}
}
args.push(data);
}
// ### Handler
// Execute the route handler and apply the result.
if (procedure.state.handle === undefined) {
return toResponse(new InternalServerError(`Path '${procedure.method}' is missing request handler.`), id);
}
return toResponse(await procedure.state.handle(...args).catch((error) => error), id);
}
}
/*
|--------------------------------------------------------------------------------
| Helpers
|--------------------------------------------------------------------------------
*/
/**
* Takes a server side request result and returns a fetch Response.
*
* @param result - Result to send back as a Response.
* @param id - Request id which can be used to identify the response.
*/
function toResponse(result: object | RelayError | Response | void, id: string): Response {
if (result === undefined) {
return new Response(
JSON.stringify({
result: null,
id,
}),
{
status: 200,
headers: {
"content-type": "application/json",
},
},
);
}
if (result instanceof Response) {
return result;
}
if (result instanceof RelayError) {
return new Response(
JSON.stringify({
error: result,
id,
}),
{
status: result.status,
headers: {
"content-type": "application/json",
},
},
);
}
return new Response(
JSON.stringify({
result,
id,
}),
{
status: 200,
headers: {
"content-type": "application/json",
},
},
);
}
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
type Config<TProcedures extends Procedure[]> = {
procedures: TProcedures;
};