154 lines
4.6 KiB
TypeScript
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;
|
|
}
|