serverus/handlers/typescript.ts

154 lines
4.6 KiB
TypeScript

/**
* Handles requests for which there are Typescript files that match
* and adhere to the `ROUTE_HANDLER` interface.
* @module
*/
import { walk } from '@std/fs';
import { delay } from '@std/async/delay';
import * as path from '@std/path';
import { getCookies } from '@std/http/cookie';
/** A `PRECHECK` must take a `Request` and `meta` data and return a `Response` IF THERE IS A PROBLEM. */
export type PRECHECK = (
request: Request,
meta: Record<string, any>
) => undefined | Response | Promise<undefined | Response>;
/** A `PRECHECK_TABLE` maps from HTTP methods to an array of `PRECHECK`s to be run. */
export type PRECHECKS_TABLE = Record<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', PRECHECK[]>;
/** A `ROUTE_HANDLER_METHOD` must take a `Request` and `meta` data and return a `Response`. */
export type ROUTE_HANDLER_METHOD = (request: Request, meta: Record<string, any>) => Promise<Response> | Response;
/** A `ROUTE_HANDLER` can export methods for handling various HTTP requests. */
export interface ROUTE_HANDLER {
PRECHECKS?: PRECHECKS_TABLE;
GET?: ROUTE_HANDLER_METHOD;
POST?: ROUTE_HANDLER_METHOD;
PUT?: ROUTE_HANDLER_METHOD;
DELETE?: ROUTE_HANDLER_METHOD;
PATCH?: ROUTE_HANDLER_METHOD;
default?: ROUTE_HANDLER_METHOD;
}
type ROUTE_HANDLER_RECORD = {
route_path: string;
route_pattern: URLPattern;
import_path: string;
module?: ROUTE_HANDLER;
};
const routes: ROUTE_HANDLER_RECORD[] = [];
let loading: boolean = false;
let all_routes_loaded: boolean = false;
/**
* Handles requests for which there are typescript files in the root tree.
*
* NOTE: On initial request the tree will be scanned and handlers loaded,
* concurrent requests should wait for the load, but until the tree has been
* scanned, requests may take longer while the load completes.
*
* @param request The incoming HTTP request
* @returns Either a response (a handler for the request path and method was found) or undefined if unhandled.
*/
export default async function handle_typescript(request: Request): Promise<Response | undefined> {
if (!all_routes_loaded) {
if (!loading) {
loading = true;
const root_directory = path.resolve(Deno.cwd());
const imports: ROUTE_HANDLER_RECORD[] = [];
for await (
const entry of walk(root_directory, {
exts: ['.ts'],
skip: [/\.test\.ts$/]
})
) {
if (entry.isFile) {
const relative_path = entry.path.substring(root_directory.length);
const route_path = relative_path
.replace(/\.ts$/, '')
//.replace(/\[(\w+)\]/g, ':$1')
.replace(/\/index$/, '')
.replace(/___/g, ':') || // required for windows, uncivilized OS that it is
'/';
const route_pattern = new URLPattern({ pathname: route_path });
const import_path = new URL('file://' + entry.path, import.meta.url).toString();
imports.push({
route_path,
route_pattern,
import_path
});
}
}
// try to sort imports such that they're registered like:
// /permissions/test
// /api/echo/hi
// /api/echo/:input
//
// we want paths with parameters to sort later than paths without
routes.push(...imports.sort((lhs, rhs) => rhs.route_path.localeCompare(lhs.route_path)));
all_routes_loaded = true;
loading = false;
}
do {
await delay(10);
} while (!all_routes_loaded);
}
for (const route_record of routes) {
const match = route_record.route_pattern.exec(request.url.replace(/\/$/, ''));
if (match) {
const method = request.method as keyof ROUTE_HANDLER;
if (!route_record.module) {
if (Deno.env.get('SERVERUS_TYPESCRIPT_IMPORT_LOGGING')) {
console.log(`${route_record.route_path} : imported: ${route_record.import_path}`);
}
}
route_record.module = route_record.module ?? await import(route_record.import_path) as ROUTE_HANDLER;
const method_handler: ROUTE_HANDLER_METHOD =
(route_record.module[method] ?? route_record.module.default) as ROUTE_HANDLER_METHOD;
if (!method_handler) {
return;
}
const cookies: Record<string, string> = getCookies(request.headers);
const query = Object.fromEntries(new URL(request.url).searchParams.entries());
const metadata = {
cookies,
params: match.pathname.groups,
query
};
const prechecks: PRECHECK[] | undefined = route_record.module.PRECHECKS?.[request.method as keyof PRECHECKS_TABLE];
if (Array.isArray(prechecks)) {
for (const precheck of prechecks) {
const result = await precheck(request, metadata);
if (result) {
return result;
}
}
}
return await method_handler(request, metadata);
}
}
}
export function unload(): void {
loading = false;
routes.splice(0, routes.length);
all_routes_loaded = false;
}