hello
Version:
Write less. Ship more.
200 lines (175 loc) • 6.3 kB
JavaScript
const _ = require('lodash')
const KoaRouter = require('koa-router')
/**
* The Router class is a wrapper over koa-router extended to include simple support for RESTful
* controllers via the `resources` method.
*/
class Router extends KoaRouter {
/**
* Generate RESTful routes for a given controller
*
* @example
* let router = new Router()
* router.resources('users', controller.Users)
* // GET /users
* // GET /users/new
* // GET /users/:id
* // GET /users/:id/edit
* // POST /users
* // PUT /users/:id
* // DELETE /users/:id
*
* @example
* let router = new Router()
* router.resources('users', controller.Users, { only: ['index', 'show'] })
* // GET /users
* // GET /users/:id
*
* @example
* let router = new Router()
* router.resources('users', controller.Users, { only: 'show' })
* // GET /users/:id
*
* @example
* let router = new Router()
* router.resources('users', controller.Users, { except: ['index', 'show', 'new', 'edit'] })
* // POST /users
* // PUT /users/:id
* // DELETE /users/:id
*
* @example
* let router = new Router()
* router.resources('users', controller.Users, { except: 'destroy' })
* // GET /users
* // GET /users/new
* // GET /users/:id
* // GET /users/:id/edit
* // POST /users
* // PUT /users/:id
*
* @param {string} path - The base path for the router
* @param {...Function} [middleware] - The middleware to be used
* @param {Controller} controller - The controller for the resource
* @param {Object} [opts] - Optional configuration options for the resource
* @param {Array|String} [opts.except] - An action or list of actions to exclude from the resource routing. Note: `only` takes precendence over `except`
* @param {Array|String} [opts.only] - A action or list of actions to include in the resource routing. Note: `only` takes precendence over `except`
* @param {String} [opts.param] - The named parameter to use in the route (default: `id`)
* @param {Boolean} [opts.api] - If this is an API router or not. If set to true, this will exclude `new` and `edit`. Default is: false
* @returns {Router} - The current Router instance (`this`)
*/
resources () {
let args = Array.prototype.slice.call(arguments)
let path = args.shift()
let routes = ['index', 'new', 'show', 'create', 'edit', 'update', 'destroy']
let param = 'id'
let opts = {}
let controller
let middleware
if (!path.startsWith('/')) {
path = `/${path}`
}
if (argumentsContainOptions(args)) {
opts = args.pop()
}
controller = args.pop()
middleware = args
if (opts.only) {
routes = _.flattenDeep([opts.only])
} else if (opts.except) {
routes = _.difference(routes, _.flattenDeep([opts.except]))
}
if (opts.param) {
param = opts.param
}
if (opts.api) {
routes = _.without(routes, 'new', 'edit')
}
if (_.includes(routes, 'index')) {
this.get.apply(this, [path].concat(middleware, [controllerMethod(controller, 'index')]))
}
if (_.includes(routes, 'new')) {
this.get.apply(this, [`${path}/new`].concat(middleware, [controllerMethod(controller, 'new')]))
}
if (_.includes(routes, 'show')) {
this.get.apply(this, [`${path}/:${param}`].concat(middleware, [controllerMethod(controller, 'show')]))
}
if (_.includes(routes, 'edit')) {
this.get.apply(this, [`${path}/:${param}/edit`].concat(middleware, [controllerMethod(controller, 'edit')]))
}
if (_.includes(routes, 'create')) {
this.post.apply(this, [path].concat(middleware, [controllerMethod(controller, 'create')]))
}
if (_.includes(routes, 'update')) {
this.put.apply(this, [`${path}/:${param}`].concat(middleware, [controllerMethod(controller, 'update')]))
this.patch.apply(this, [`${path}/:${param}`].concat(middleware, [controllerMethod(controller, 'update')]))
}
if (_.includes(routes, 'destroy')) {
this.delete.apply(this, [`${path}/:${param}`].concat(middleware, [controllerMethod(controller, 'destroy')]))
}
return this
}
}
/**
* Determine if the arguments list contains a trailing `opts` object.
* Possible combinations are as below (f = function, c = class, a = array, o = object)
*
* f(middleware), f(middleware), c(controller), o(opts) => true
* f(middleware), f(middleware), o(controller), o(opts) => true
*
* f(middleware), c(controller), o(opts) => true
* f(middleware), o(controller), o(opts) => true
*
* f(middleware), f(middleware), c(controller) => false
* f(middleware), f(middleware), o(controller) => false
* f(middleware), c(controller) => false
* f(middleware), o(controller) => false
*
* c(controller), o(opts) => true
* o(controller), o(opts) => true
*
* c(controller) => false
* o(controller) => false
*
* @returns {Boolean} - Whether the args array contains a trailing opts object
*/
function argumentsContainOptions (args) {
if (args.length < 2) { // controller-only
return false
}
if (_.isPlainObject(args[args.length - 1])) { // controller or options
if (isClass(args[args.length - 2]) || _.isPlainObject(args[args.length - 2])) { // controller
return true
}
}
return false
}
function isClass (fn) {
return _.isFunction(fn) && /^class\s/.test(Function.prototype.toString.call(fn))
}
/**
* Returns a wrapper to be used in the router for calling a given controller method.
*
* @param {Object|Controller} controller - The controller
* @param {String} method - The method on the controller to call
* @returns {Function} The controller method to call
*/
function controllerMethod (controller, method) {
if (controller && controller[method]) {
return controller[method]
}
// Handle hello-based Controller classes
if (isClass(controller) && controller.action) {
return controller.action(method)
}
return notImplemented
}
/**
* Simple function to throw a `501 Not Implemented` error
*
* @throws `501 Not Implemented` error
*/
function notImplemented (ctx) {
return ctx.throw(501)
}
module.exports = Router