koa-rest-router
Version:
Most powerful, flexible and composable router for building enterprise RESTful APIs easily!
541 lines (515 loc) • 16.1 kB
JavaScript
/*!
* koa-rest-router <https://github.com/tunnckoCore/koa-rest-router>
*
* Copyright (c) Charlike Mike Reagent <@tunnckoCore> (https://i.am.charlike.online)
* Released under the MIT license.
*/
let util = require('util')
let utils = require('./utils')
/**
* > Initialize `KoaRestRouter` with optional `options`,
* directly passed to [koa-better-router][] and this package
* inherits it. So you have all methods and functionality from
* the awesome [koa-better-router][] middleware.
*
* **Example**
*
* ```js
* let Router = require('koa-rest-router')
* let api = Router({ prefix: '/api/v1' })
*
* // - can have multiples middlewares
* // - can have both old and modern middlewares combined
* api.resource('companies', {
* index: function (ctx, next) {},
* show: function (ctx, next) {},
* create: function (ctx, next) {}
* // ... and etc
* })
*
* console.log(api.routes.length) // 7
* console.log(api.resources.length) // 1
*
* api.resource('profiles', {
* index: function (ctx, next) {},
* show: function (ctx, next) {},
* create: function (ctx, next) {}
* // ... and etc
* })
*
* console.log(api.routes.length) // 14
* console.log(api.resources.length) // 2
*
* let Koa = require('koa') // Koa v2
* let app = new Koa()
*
* let basic = Router() // prefix is `/` by default
* basic.extend(api)
*
* app.use(api.middleware())
* app.use(basic.middleware())
*
* app.listen(4444, () => {
* console.log('Open http://localhost:4444 and try')
* // will output 2x14 links
* // - 14 links on `/api/v1` prefix
* // - 14 links on `/` prefix
* api.routes.forEach((route) => {
* console.log(`${route.method} http://localhost:4444${route.path}`)
* })
* basic.routes.forEach((route) => {
* console.log(`${route.method} http://localhost:4444${route.path}`)
* })
* })
* ```
*
* @param {Object} `[options]` passed directly to [koa-better-router][],
* in addition we have 2 more options here.
* @param {Object} `[options.methods]` override request methods to be used
* @param {Object} `[options.map]` override controller methods to be called
* @api public
*/
function KoaRestRouter (options) {
if (!(this instanceof KoaRestRouter)) {
return new KoaRestRouter(options)
}
utils.Router.call(this, utils.mergeOptions({
methods: utils.defaultRequestMethods,
map: utils.defaultControllerMap
}, options))
this.resources = []
}
util.inherits(KoaRestRouter, utils.Router)
/**
* > Core method behind `.resource` for creating single
* resource with a `name`, but without adding it to `this.routes` array.
* You can override any defaults - default request methods and
* default controller methods, just by passing
* respectively `opts.methods` object and `opts.map` object.
* It uses [koa-better-router][]'s `.createRoute` under the hood.
*
* **Example**
*
* ```js
* let router = require('koa-rest-router')({
* prefix: '/api'
* }).loadMethods()
*
* // The server part
* let body = require('koa-better-body')
* let Koa = require('koa')
* let app = new Koa()
*
* // override request methods
* let methods = {
* put: 'POST'
* del: 'POST'
* }
*
* // override controller methods
* let map = {
* index: 'list',
* show: 'read',
* remove: 'destroy'
* }
*
* // notice the body should be invoked explicitly
* // with or without options object, no matter
* let updateMiddlewares = [body(), (ctx, next) => {
* ctx.body = `This method by default is triggered with PUT requests only.`
* ctx.body = `${ctx.body} But now it is from POST request.`
* return next()
* }, function * (next) => {
* this.body = `${this.body} Incoming data is`
* this.body = `${this.body} ${JSON.stringify(this.request.fields, null, 2)}`
* yield next
* }]
*
* // create actual resource
* let cats = router.createResource('cats', {
* list: [
* (ctx, next) => {
* ctx.body = `This is GET ${ctx.route.path} route with multiple middlewares`
* return next()
* },
* function * (next) {
* this.body = `${this.body} and combining old and modern middlewares.`
* yield next
* }
* ],
* read: (ctx, next) => {
* ctx.body = `This is ${ctx.route.path} route.`
* ctx.body = `${ctx.body} And param ":cat" is ${ctx.params.cat}.`
* ctx.body = `${ctx.body} By default this method is called "show".`
* return next()
* },
* update: updateMiddlewares,
* destroy: (ctx, next) => {
* ctx.body = `This route should be called with DELETE request, by default.`
* ctx.body = `${ctx.body} But now it request is POST.`
* return next()
* }
* }, {map: map, methods: methods})
*
* console.log(cats)
* // => array of "Route Objects"
*
* // router.routes array is empty
* console.log(router.getRoutes()) // => []
*
* // register the resource
* router.addResource(cats)
*
* console.log(router.routes.length) // => 7
* console.log(router.getRoutes().length) // => 7
* console.log(router.getRoutes()) // or router.routes
* // => array of "Route Objects"
*
* app.use(router.middleware())
*
* app.listen(5000, () => {
* console.log(`Server listening on http://localhost:5000`)
* console.log(`Try to open these routes:`)
*
* router.routes.forEach((route) => {
* console.log(`${route.method}` http://localhost:5000${route.path}`)
* }))
* })
* ```
*
* @param {String|Object} `name` name of the resource or `ctrl`
* @param {Object} `ctrl` controller object to be called on each endpoint, or `opts`
* @param {Object} `opts` optional, merged with options from constructor
* @return {KoaRestRouter} `this` instance for chaining
* @api public
*/
KoaRestRouter.prototype.createResource = function createResource (name, ctrl, opts) {
if (typeof name === 'object') {
opts = ctrl
ctrl = name
name = '/'
}
if (typeof name !== 'string') {
name = '/'
}
let _name = name[0] === '/' ? name.slice(1) : name
let route = name !== '/' ? utils.inflection.pluralize(_name) : ''
let param = name !== '/' ? ':' + utils.inflection.singularize(_name) : ':id'
let src = utils.createResourceRoutes(route, param, ctrl)(this, opts)
// add them to cache
src.name = route === '' ? '/' : route
this.resources.push(src)
// just return them without
// adding them to `this.routes`
return src
}
/**
* > Simple method that is alias of `.addRoutes` and `.addResources`,
* but for adding single resource.
* It can accepts only one `resource` object.
*
* **Example**
*
* ```js
* let Router = require('koa-rest-router')
* let api = new Router({
* prefix: '/'
* })
*
* console.log(api.resources.length) // 0
* console.log(api.routes.length) // 0
*
* api.addResource(api.createResource('dragons'))
*
* console.log(api.resources.length) // 1
* console.log(api.routes.length) // 7
*
* console.log(api.getResource('dragons'))
* // array of route objects
* // => [
* // { prefix: '/', route: '/dragons', path: '/dragons', ... }
* // { prefix: '/', route: '/dragons/:dragon', path: '/dragons/:dragon', ... }
* // ... and 5 more routes
* // ]
* ```
*
* @param {Array} `resource` array of route objects, known as _"Resource Object"_
* @return {KoaRestRouter} `this` instance for chaining
* @api public
*/
KoaRestRouter.prototype.addResource = function addResource (resource) {
return this.addRoutes(resource)
}
/**
* > Get single resource by `name`. Special case is resource
* to the `/` prefix. So pass `/` as `name`. See more on what
* are the _"Route Objects"_ in the [koa-better-router][] docs.
* What that method returns, I call _"Resource Object"_ - array
* of _"Route Objects"_
*
* **Example**
*
* ```js
* let api = require('koa-rest-router')({
* prefix: '/api/v2'
* })
*
* let frogs = api.createResource('frogs')
* let dragons = api.createResource('dragons')
*
* console.log(api.getResource('frogs'))
* // array of route objects
* // => [
* // { prefix: '/api/v2', route: '/frogs', path: '/api/v2/frogs', ... }
* // { prefix: '/api/v2', route: '/frogs/:frog', path: '/api/v2/frogs/:frog', ... }
* // ... and 5 more routes
* // ]
*
* console.log(api.getResources().length) // 2
* ```
*
* @param {String} `name` name of the resource, plural
* @return {Array|Null} if resource with `name` not found `null, otherwise
* array of route objects - that array is known as Resource Object
* @api public
*/
KoaRestRouter.prototype.getResource = function getResource (name) {
let res = null
for (let resource of this.resources) {
if (resource.name === name) {
res = resource
break
}
}
return res
}
/**
* > Creates a resource using `.createResource` and adds
* the resource routes to the `this.routes` array, using `.addResource`.
* This is not an alias! It is combination of two methods. Methods
* that are not defined in given `ctrl` (controller) returns
* by default `501 Not Implemented`. You can override any
* defaults - default request methods and default controller methods,
* just by passing respectively `opts.methods` object and `opts.map` object.
*
* **Example**
*
* ```js
* let Router = require('koa-rest-router')
*
* let api = new Router({ prefix: '/api/v3' })
* let router = new Router() // on `/` prefix by default
*
* // All of the controller methods
* // can be remap-ed. using `opts.map`
* // try to pass `{ map: { index: 'home' } }` as options
*
* api.resource('users', {
* // GET /users
* index: [(ctx, next) => {}, (ctx, next) => {}],
*
* // GET /users/new
* new: (ctx, next) => {},
*
* // POST /users
* create: (ctx, next) => {},
*
* // GET /users/:user
* show: [(ctx, next) => {}, function * (next) {}],
*
* // GET /users/:user/edit
* edit: (ctx, next) => {},
*
* // PUT /users/:user
* // that `PUT` can be changed `opts.methods.put: 'post'`
* update: (ctx, next) => {},
*
* // DELETE /users/:user
* // that `DELETE` can be changed `opts.methods.delete: 'post'`
* remove: (ctx, next) => {}
* })
*
* // notice the `foo` method
* router.resource({
* // GET /
* foo: [
* (ctx, next) => {
* ctx.body = `GET ${ctx.route.path}`
* return next()
* },
* function * (next) {
* ctx.body = `${this.body}! Hello world!`
* yield next
* }
* ],
* // GET /:id, like /123
* show: (ctx, next) => {
* ctx.body = JSON.stringify(ctx.params, null, 2)
* return next()
* }
* }, {
* map: {
* index: 'foo'
* }
* })
*
* api.routes.forEach(route => console.log(route.method, route.path))
* router.routes.forEach(route => console.log(route.method, route.path))
*
* // Wanna use only one router?
* let fooRouter = new Router()
* let Koa = require('koa')
* let app = new Koa()
*
* fooRouter.addRoutes(api.getResources(), router.getRoutes())
*
* console.log(fooRouter.routes)
* console.log(fooRouter.routes.length) // 14
*
* app.use(fooRouter.middleware())
*
* app.listen(4433, () => {
* console.log('Cool server started at 4433. Try these routes:')
*
* fooRouter.routes.forEach((route) => {
* console.log(`${route.method} http://localhost:4433${route.path}`)
* })
* })
* ```
*
* @param {String|Object} `name` name of the resource or `ctrl`
* @param {Object} `ctrl` controller object to be called on each endpoint, or `opts`
* @param {Object} `opts` optional, merged with options from constructor
* @return {KoaRestRouter} `this` instance for chaining
* @api public
*/
KoaRestRouter.prototype.resource = function resource_ (name, ctrl, opts) {
let resource = this.createResource.apply(this, arguments)
return this.addResource(resource)
}
/**
* > Just an alias of [koa-better-router][]'s' `.addRoutes` method.
*
* @param {Array} `...args` any number of arguments (arrays of route objects)
* @return {KoaRestRouter} `this` instance for chaining
* @api public
*/
KoaRestRouter.prototype.addResources = function addResources () {
return this.addRoutes.apply(this, arguments)
}
/**
* > As we have `.getRoutes` method for getting `this.routes`,
* so we have `.getResources` for getting `this.resources` array, too.
* Each `.createResource` returns array of route objects with length of 7,
* so 7 routes. So if you call `.createResource` two times
* the `this.resources` (what this method returns) will contain 2 arrays
* with 7 routes in each of them.
*
* **Example**
*
* ```js
* let router = require('koa-rest-router')().loadMethods()
*
* console.log(router.routes.length) // 0
* console.log(router.getRoutes().length) // 0
*
* console.log(router.resources.length) // 0
* console.log(router.getResources().length) // 0
*
* router.get('/about', (ctx, next) => {})
* router.resource('dogs')
* router.resource('cats')
*
* console.log(router.routes.length) // 15
* console.log(router.getRoutes().length) // 15
*
* console.log(router.resources.length) // 2
* console.log(router.getResources().length) // 2
* ```
*
* @return {Array} array of arrays of route objects
* @api public
*/
KoaRestRouter.prototype.getResources = function getResources () {
return this.resources
}
/**
* > Powerful method for grouping couple of resources into
* one resource endpoint. For example you have `/cats` and `/dogs`
* endpoints, but you wanna create `/cats/:cat/dogs/:dog` endpoint,
* so you can do such things with that. You can group infinite
* number of resources. Useful methods that gives you what you
* should pass as arguments here are `.createResource`, `.createRoute`,
* `.getResources`, `.getResource` and `.getRoutes`.
* **Note:** Be aware of that it replaces middlewares of `dest`
* with the middlewares of last `src`.
*
* **Example**
*
* ```js
* let router = require('koa-rest-router')({ prefix: '/api/v3'})
*
* let departments = router.createResource('departments')
* let companies = router.createResource('companies')
* let profiles = router.createResource('profiles')
* let clients = router.createResource('clients')
* let users = router.createResource('users')
* let cats = router.createResource('cats')
* let dogs = router.createResource('dogs')
*
* // endpoint: /companies/:company/departments/:department
* let one = router.groupResources(companies, departments)
*
* // endpoint: /profiles/:profile/clients/:client/cats/:cat
* let two = router.groupResources(profiles, clients, cats)
*
* // crazy? huh, AWESOME!
* // endpoint: /companies/:company/departments/:department/profiles/:profile/clients/:client/cats/:cat
* let foo = router.groupResources(one, two)
*
* // but actually just "register" `one` and `foo`
* // so you WON'T have `/profiles/:profile/clients/:client/cats/:cat`
* // endpoint in your API
* router.addRoutes(one, foo)
*
* // Server part
* let Koa = require('koa')
* let app = new Koa()
*
* app.use(router.middleware())
*
* app.listen(4000, () => {
* console.log(`Mega API server on http://localhost:4000`)
* console.log(`Checkout these routes:`)
*
* // it will output 14 links
* router.getRoutes().forEach((route) => {
* console.log(`${route.method} http://localhost:4000${route.path}`)
* })
* })
* ```
*
* @param {Array} `dest` array of _"Route Objects"_ or _"Resource Object"_ (both are arrays)
* @param {Array} `src1` array of _"Route Objects"_ or _"Resource Object"_ (both are arrays)
* @param {Array} `src2` array of _"Route Objects"_ or _"Resource Object"_ (both are arrays)
* @return {Array} new array with grouped resources
* @api public
*/
KoaRestRouter.prototype.groupResources = function groupResources (dest, src1, src2) {
return dest.map((destRoute, index) => {
let route = utils.updateRoute(this, destRoute)
route = this.groupRoutes(route, src1[index])
/* istanbul ignore next */
return src2 && Array.isArray(src2)
? this.groupRoutes(utils.updateRoute(this, route), src2[index])
: route
})
}
/**
* Expose `KoaRestRouter` constructor
*
* @type {Function}
* @api private
*/
module.exports = KoaRestRouter