@tunframework/tun-rest-router
Version:
rest router for tun
147 lines (146 loc) • 5.17 kB
JavaScript
// @ts-check
import { HttpError, HttpMethod } from '@tunframework/tun';
import { matchRoute, pathToRegexp } from './route-utils.js';
export class RestifyRouter {
_routes = [];
// temp prefix, consumed in addRoute()
_prefix = '';
_methods;
constructor({ prefix = '', methods } = {}) {
this.prefix(prefix);
this._methods = methods;
}
prefix(_prefix) {
this._prefix = _prefix || '';
return this;
}
routes() {
return (ctx, next) => {
let route = matchRoute(ctx.req.method, ctx.req.path, this._routes);
if (!route) {
return next();
}
ctx.state.matchedRoute = route;
if (ctx.res.status === 404) {
ctx.res.status = 200;
}
let slugs = route.slugNames.reduce((p, n, nIndex) => {
p[n] = route.slugValues[nIndex];
return p;
}, {});
ctx.req.slugs = slugs;
return route.handler.call(null, ctx, next);
};
}
addRoute(methods, pathname, handler) {
this._routes.push({
// ...pathToRegexp(pathname),
...pathToRegexp((this._prefix || '') + pathname),
methods: Array.isArray(methods) ? methods : [methods],
slugValues: [],
handler
});
return this;
}
get(pathname, handler) {
return this.addRoute('GET', pathname, handler);
}
head(pathname, handler) {
return this.addRoute('HEAD', pathname, handler);
}
post(pathname, handler) {
return this.addRoute('POST', pathname, handler);
}
put(pathname, handler) {
return this.addRoute('PUT', pathname, handler);
}
delete(pathname, handler) {
return this.addRoute('DELETE', pathname, handler);
}
// connect(pathname: string, handler: TunComposable<TunContext>) {
// return this.addRoute('CONNECT', pathname, handler);
// }
options(pathname, handler) {
return this.addRoute('OPTIONS', pathname, handler);
}
// trace(pathname: string, handler: TunComposable<TunContext>) {
// return this.addRoute('TRACE', pathname, handler);
// }
patch(pathname, handler) {
return this.addRoute('PATCH', pathname, handler);
}
/**
* Fufill response header if all middleware resolved and `ctx.status` is empty or 404(not_found).
*/
allowedMethods(options = {}) {
const implemented = this._methods || Object.keys(HttpMethod);
return async function allowedMethods(ctx, next) {
await next();
if (ctx.res.status || ctx.res.status !== 404) {
return;
}
const allowed = {};
const matchedRoute = ctx.state.matchedRoute;
if (matchedRoute) {
matchedRoute.methods.forEach((method) => {
allowed[method] = method;
});
}
const allowedArr = Object.keys(allowed);
if (implemented.indexOf(ctx.req.method) > -1) {
if (options.throw) {
let throwable = null;
if (typeof options.notImplemented === 'function') {
throwable = options.notImplemented();
}
else {
throwable = new HttpError({ status: 501 });
}
throw throwable;
}
else {
ctx.res.status = 501;
ctx.res.set('Allow', allowedArr);
}
}
else if (allowedArr.length) {
if (ctx.req.method === 'OPTIONS') {
ctx.res.status = 200;
ctx.body = '';
ctx.res.set('Allow', allowedArr);
}
else if (!allowed[ctx.req.method]) {
if (options.throw) {
let throwable = null;
if (typeof options.notImplemented === 'function') {
throwable = options.notImplemented();
}
else {
throwable = new HttpError({ status: 405 });
}
throw throwable;
}
else {
ctx.res.status = 405;
ctx.res.set('Allow', allowedArr);
}
}
}
};
}
}
export let loadRestifyRoutes = async (pathname) => {
let M = await import(pathname);
let list = Object.keys(M)
.filter((k) => M[k] instanceof RestifyRouter)
.map((k) => M[k]);
if (list.length === 1) {
return list[0];
}
// compose
let r = new RestifyRouter();
for (const item of list) {
r._routes.push(...item._routes);
}
return r;
};