'use strict'; const is = require('is-type-of'); const Router = require('./router'); const utility = require('utility'); const inflection = require('inflection'); const assert = require('assert'); const utils = require('./utils'); const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ]; const REST_MAP = { index: { suffix: '', method: 'GET', }, new: { namePrefix: 'new_', member: true, suffix: 'new', method: 'GET', }, create: { suffix: '', method: 'POST', }, show: { member: true, suffix: ':id', method: 'GET', }, edit: { member: true, namePrefix: 'edit_', suffix: ':id/edit', method: 'GET', }, update: { member: true, namePrefix: '', suffix: ':id', method: [ 'PATCH', 'PUT' ], }, destroy: { member: true, namePrefix: 'destroy_', suffix: ':id', method: 'DELETE', }, }; /** * FIXME: move these patch into @eggjs/router */ class EggRouter extends Router { /** * @class * @param {Object} opts - Router options. * @param {Application} app - Application object. */ constructor(opts, app) { super(opts); this.app = app; this.patchRouterMethod(); } patchRouterMethod() { // patch router methods to support generator function middleware and string controller METHODS.concat([ 'all' ]).forEach(method => { this[method] = (...args) => { const splited = spliteAndResolveRouterParams({ args, app: this.app }); // format and rebuild params args = splited.prefix.concat(splited.middlewares); return super[method](...args); }; }); } /** * Create and register a route. * @param {String} path - url path * @param {Array} methods - Array of HTTP verbs * @param {Array} middlewares - * @param {Object} opts - * @return {Route} this */ register(path, methods, middlewares, opts) { // patch register to support generator function middleware and string controller middlewares = Array.isArray(middlewares) ? middlewares : [ middlewares ]; middlewares = convertMiddlewares(middlewares, this.app); path = Array.isArray(path) ? path : [ path ]; path.forEach(p => super.register(p, methods, middlewares, opts)); return this; } /** * restful router api * @param {String} name - Router name * @param {String} prefix - url prefix * @param {Function} middleware - middleware or controller * @example * ```js * app.resources('/posts', 'posts') * app.resources('posts', '/posts', 'posts') * app.resources('posts', '/posts', app.role.can('user'), app.controller.posts) * ``` * * Examples: * * ```js * app.resources('/posts', 'posts') * ``` * * yield router mapping * * Method | Path | Route Name | Controller.Action * -------|-----------------|----------------|----------------------------- * GET | /posts | posts | app.controller.posts.index * GET | /posts/new | new_post | app.controller.posts.new * GET | /posts/:id | post | app.controller.posts.show * GET | /posts/:id/edit | edit_post | app.controller.posts.edit * POST | /posts | posts | app.controller.posts.create * PATCH | /posts/:id | post | app.controller.posts.update * DELETE | /posts/:id | post | app.controller.posts.destroy * * app.router.url can generate url based on arguments * ```js * app.router.url('posts') * => /posts * app.router.url('post', { id: 1 }) * => /posts/1 * app.router.url('new_post') * => /posts/new * app.router.url('edit_post', { id: 1 }) * => /posts/1/edit * ``` * @return {Router} return route object. * @since 1.0.0 */ resources(...args) { const splited = spliteAndResolveRouterParams({ args, app: this.app }); const middlewares = splited.middlewares; // last argument is Controller object const controller = splited.middlewares.pop(); let name = ''; let prefix = ''; if (splited.prefix.length === 2) { // router.get('users', '/users') name = splited.prefix[0]; prefix = splited.prefix[1]; } else { // router.get('/users') prefix = splited.prefix[0]; } for (const key in REST_MAP) { const action = controller[key]; if (!action) continue; const opts = REST_MAP[key]; let formatedName; if (opts.member) { formatedName = inflection.singularize(name); } else { formatedName = inflection.pluralize(name); } if (opts.namePrefix) { formatedName = opts.namePrefix + formatedName; } prefix = prefix.replace(/\/$/, ''); const path = opts.suffix ? `${prefix}/${opts.suffix}` : prefix; const method = Array.isArray(opts.method) ? opts.method : [ opts.method ]; this.register(path, method, middlewares.concat(action), { name: formatedName }); } return this; } /** * @param {String} name - Router name * @param {Object} params - more parameters * @example * ```js * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) * => /posts/1/edit?name=foo&page=2 * router.url('posts', { name: 'foo&1', page: 2 }) * => /posts?name=foo%261&page=2 * ``` * @return {String} url by path name and query params. * @since 1.0.0 */ url(name, params) { const route = this.route(name); if (!route) return ''; const args = params; let url = route.path; assert(!is.regExp(url), `Can't get the url for regExp ${url} for by name '${name}'`); const queries = []; if (typeof args === 'object' && args !== null) { const replacedParams = []; url = url.replace(/:([a-zA-Z_]\w*)/g, function($0, key) { if (utility.has(args, key)) { const values = args[key]; replacedParams.push(key); return utility.encodeURIComponent(Array.isArray(values) ? values[0] : values); } return $0; }); for (const key in args) { if (replacedParams.includes(key)) { continue; } const values = args[key]; const encodedKey = utility.encodeURIComponent(key); if (Array.isArray(values)) { for (const val of values) { queries.push(`${encodedKey}=${utility.encodeURIComponent(val)}`); } } else { queries.push(`${encodedKey}=${utility.encodeURIComponent(values)}`); } } } if (queries.length > 0) { const queryStr = queries.join('&'); if (!url.includes('?')) { url = `${url}?${queryStr}`; } else { url = `${url}&${queryStr}`; } } return url; } pathFor(name, params) { return this.url(name, params); } } /** * 1. split (name, url, ...middleware, controller) to * { * prefix: [name, url] * middlewares [...middleware, controller] * } * * 2. resolve controller from string to function * * @param {Object} options inputs * @param {Object} options.args router params * @param {Object} options.app egg application instance * @return {Object} prefix and middlewares */ function spliteAndResolveRouterParams({ args, app }) { let prefix; let middlewares; if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) { // app.get(name, url, [...middleware], controller) prefix = args.slice(0, 2); middlewares = args.slice(2); } else { // app.get(url, [...middleware], controller) prefix = args.slice(0, 1); middlewares = args.slice(1); } // resolve controller const controller = middlewares.pop(); middlewares.push(resolveController(controller, app)); return { prefix, middlewares }; } /** * resolve controller from string to function * @param {String|Function} controller input controller * @param {Application} app egg application instance * @return {Function} controller function */ function resolveController(controller, app) { if (is.string(controller)) { const actions = controller.split('.'); let obj = app.controller; actions.forEach(key => { obj = obj[key]; if (!obj) throw new Error(`controller '${controller}' not exists`); }); controller = obj; } // ensure controller is exists if (!controller) throw new Error('controller not exists'); return controller; } /** * 1. ensure controller(last argument) support string * - [url, controller]: app.get('/home', 'home'); * - [name, url, controller(string)]: app.get('posts', '/posts', 'posts.list'); * - [name, url, controller]: app.get('posts', '/posts', app.controller.posts.list); * - [name, url(regexp), controller]: app.get('regRouter', /\/home\/index/, 'home.index'); * - [name, url, middleware, [...], controller]: `app.get(/user/:id', hasLogin, canGetUser, 'user.show');` * * 2. make middleware support generator function * * @param {Array} middlewares middlewares and controller(last middleware) * @param {Application} app egg application instance * @return {Array} middlewares */ function convertMiddlewares(middlewares, app) { // ensure controller is resolved const controller = resolveController(middlewares.pop(), app); // make middleware support generator function middlewares = middlewares.map(utils.middleware); const wrappedController = (ctx, next) => { return utils.callFn(controller, [ ctx, next ], ctx); }; return middlewares.concat([ wrappedController ]); } module.exports = EggRouter;