123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- '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;
|