koa-detour
Version:
Resource-oriented routing for Koa 2+
156 lines (125 loc) • 3.88 kB
JavaScript
const url = require("url");
const methods = require("methods").map(m => m.toUpperCase());
const Route = require("./route");
class Detour {
constructor (options = {}) {
this._middleware = [];
this._routes = [];
this._handlers = Object.create(defaultHandlers);
this._routeOptions = {
strict: options.strict,
sensitive: options.caseSensitive,
};
}
middleware () {
return (ctx, next) => this.dispatch(ctx, next);
}
async dispatch (ctx, next) {
const path = url.parse(ctx.req.url).pathname;
const route = this._routes.find(r => r.match(path));
if (route == null) return next();
ctx.route = route;
ctx.resource = route.resource;
ctx.params = route.params(path);
const method = ctx.req.method.toUpperCase();
if (ctx.resource[method] == null) {
switch (method) {
case "HEAD": return this._handlers.HEAD(ctx, next, this);
case "OPTIONS": return this._handlers.OPTIONS(ctx);
default: return this._handlers.methodNotAllowed(ctx);
}
}
// call each middleware in turn
for (const fn of this._middleware) await fn(ctx);
// then call the relevant HTTP handler on the resource
return ctx.resource[method](ctx);
}
// add a general middleware
use (fn) {
if (typeof fn !== "function") {
throw new TypeError("`use` requires a function");
}
this._middleware.push(fn);
return this;
}
// tiny helper for plugins that want to call several methods on router
apply (fn, ...args) {
if (typeof fn !== "function") {
throw new TypeError("`apply` requires a function");
}
fn(this, ...args);
return this;
}
route (path, resource) {
validatePath(path);
validateResource(resource);
const route = new Route(path, resource, this._routeOptions);
this._routes.push(route);
return this;
}
handle (type, handler) {
if (!defaultHandlers.hasOwnProperty(type)) {
throw new Error(`\`type\` argument must be one of 'OPTIONS', 'HEAD', 'methodNotAllowed', found: ${type}`);
}
if (typeof handler !== "function") {
throw new Error("Handler must be a function");
}
this._handlers[type] = handler;
return this;
}
collection (path, pairObj) {
if (pairObj.collection == null) {
throw new Error(`Detour.collection() requires an object with a \`collection\` property. Path was: ${path}`);
}
if (pairObj.member) {
this.route(path, pairObj.member);
}
this.route(parentPath(path), pairObj.collection);
return this;
}
}
const defaultHandlers = {
methodNotAllowed (ctx) {
const header = getMethods(ctx.resource).join(",");
ctx.set("Allow", header);
ctx.status = 405;
ctx.body = "Method Not Allowed";
},
OPTIONS (ctx) {
const header = getMethods(ctx.resource).join(",");
ctx.set("Allow", header);
ctx.status = 200;
ctx.body = `Allow: ${header}`;
},
HEAD (ctx, next, router) {
if (ctx.resource.GET == null) {
return router._handlers.methodNotAllowed(ctx);
}
ctx.req.method = "GET";
// Node.js strips off the body automatically. See test case.
return router.dispatch(ctx, next);
},
};
function parentPath (path){
const pieces = path.split("/");
const last = pieces.pop();
if (!last) {
pieces.pop();
}
return pieces.join("/");
}
function validateResource (resource) {
if (!methods.some(m => resource[m])) {
throw new Error("Resource should have at least one key with a valid HTTP verb");
}
}
function validatePath (path) {
if (typeof path === "string") return;
if (Array.isArray(path)) return;
if (Object.prototype.toString.call(path) === "[object RegExp]") return;
throw new Error(`Invalid path: ${path}`);
}
function getMethods (resource) {
return methods.filter(m => resource[m]);
}
module.exports = Detour;