import InfinityHeaders from "@infinity-beyond/modules/networking/infinity_headers.ts";
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type PathParams<Path extends string, Params = {[K in ExtractParams<Path>]: K extends string ? string : never}> = Params
type WithParams = <T extends string, K extends Record<string, string> = PathParams<T>>(path: T, handler: RouteHandler<K>) => void
export class REST_Wrapper {
readonly routes: Map<RegExp, Route> = new Map<RegExp, Route>()
protected get = this.add_handler.bind(this, 'GET') as WithParams;
protected post = this.add_handler.bind(this, 'POST') as WithParams;
protected put = this.add_handler.bind(this, 'PUT') as WithParams;
protected patch = this.add_handler.bind(this, 'PATCH') as WithParams;
protected delete = this.add_handler.bind(this, 'DELETE') as WithParams;
protected head = this.add_handler.bind(this, 'HEAD') as WithParams;
protected options = this.add_handler.bind(this, 'OPTIONS') as WithParams;
protected any = this.add_handler.bind(this, 'ANY') as WithParams;
private add_handler(method: REST_METHOD, path: string, handler: RouteHandler) {
const param_variables: string[] = [];
const regex = new RegExp('^' + path.replace(/\:(\w+)/g, (_, b) => {
param_variables.push(b);
return `([^\\\/]+)`;
}) + '$');
this.routes.set(regex, {
method,
path,
param_variables,
handler,
});
}
/**
* Must be exported from `[...path].tsx` in the relevant path
*
* Example:
* `export const handler = [Ledger].REST.handler;`
*/
get handler() {
return this.__handler.bind(this);
}
private async __handler(request: Request, ctx: InfinityContext) {
const relative_pathname = ctx.url.pathname.replace(ctx.route.replace(/\/:path\*$/, ''), '');
return this.handle(relative_pathname, request, ctx);
}
private async handle(pathname: string, request: Request, ctx: InfinityContext) {
try {
const no_query_pathname = pathname.replace(/\?.+$/, '') || '/';
const allow_methods = this.routes.entries().toArray().filter(([ regex ]) => regex.test(no_query_pathname)).map(([ _, route ]) => route.method).join(', ');
const allow_headers = "Access-Control-Allow-Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With";
if(request.method == 'OPTIONS') {
const cors_response = new Response(undefined, {
status: 204
});
InfinityHeaders.apply(cors_response.headers, {
"Access-Control-Allow-Origin": request.headers.get("Origin") || "*",
"Access-Control-Allow-Methods": allow_methods,
'Access-Control-Allow-Headers': allow_headers,
'Access-Control-Allow-Credentials': "true",
})
return cors_response;
}
const valid_routes = this.routes.entries().toArray().filter(([ regex, route ]) => (
route.method == 'ANY' || route.method == request.method
) && regex.test(no_query_pathname));
for(const [regex, route] of valid_routes) {
const [_, ...params] = no_query_pathname.match(regex) || [];
ctx.params = Object.assign({},
...route.param_variables.map((v, i) => ({[v]: params[i]}))
)
const response= await route.handler(request, ctx, () => '__NEXT__');
if(response instanceof Response) {
InfinityHeaders.apply(response.headers, {
"Access-Control-Allow-Origin": request.headers.get("Origin") || "*",
"Access-Control-Allow-Methods": allow_methods,
'Access-Control-Allow-Headers': allow_headers,
'Access-Control-Allow-Credentials': "true",
})
return response;
}
if(response == '__NEXT__') continue;
throw new Deno.errors.UnexpectedEof();
}
throw new Deno.errors.NotFound();
} catch(e: any) {
if(e instanceof Deno.errors.InvalidData) {
return Response.json({}, {
status: 400
});
}
if(e instanceof Deno.errors.PermissionDenied) {
return Response.json({}, {
status: 403
});
}
if(e instanceof Deno.errors.NotFound) {
return Response.json({}, {
status: 404
});
}
console.warn(`[${request.url.replace(/\?.+$/, '')}] ${e.message || 'An unexpected error ocurred'}`);
return Response.json({}, { status: 500 });
}
}
}
type Next = '__NEXT__';
type RouteHandler<T extends Record<string, any> = Record<string, any>> = (request: Request, ctx: InfinityContext<T>, next: () => Next) => Promise<Response | Next> | Response | Next;
type REST_METHOD = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'ANY'
interface Route {
method: REST_METHOD
path: string
param_variables: string[]
handler: RouteHandler<any>
}