1 | /**
|
2 | * RESTful resource routing middleware for koa.
|
3 | *
|
4 | * @author Alex Mingoia <talk@alexmingoia.com>
|
5 | * @link https://github.com/alexmingoia/koa-router
|
6 | */
|
7 |
|
8 | const compose = require('koa-compose');
|
9 | const HttpError = require('http-errors');
|
10 | const methods = require('methods');
|
11 | const { pathToRegexp } = require('path-to-regexp');
|
12 | const Layer = require('./layer');
|
13 | const debug = require('debug')('koa-router');
|
14 |
|
15 | /**
|
16 | * @module koa-router
|
17 | */
|
18 |
|
19 | module.exports = Router;
|
20 |
|
21 | /**
|
22 | * Create a new router.
|
23 | *
|
24 | * @example
|
25 | *
|
26 | * Basic usage:
|
27 | *
|
28 | * ```javascript
|
29 | * const Koa = require('koa');
|
30 | * const Router = require('@koa/router');
|
31 | *
|
32 | * const app = new Koa();
|
33 | * const router = new Router();
|
34 | *
|
35 | * router.get('/', (ctx, next) => {
|
36 | * // ctx.router available
|
37 | * });
|
38 | *
|
39 | * app
|
40 | * .use(router.routes())
|
41 | * .use(router.allowedMethods());
|
42 | * ```
|
43 | *
|
44 | * @alias module:koa-router
|
45 | * @param {Object=} opts
|
46 | * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches
|
47 | * @param {String=} opts.prefix prefix router paths
|
48 | * @param {String|RegExp=} opts.host host for router match
|
49 | * @constructor
|
50 | */
|
51 |
|
52 | function Router(opts = {}) {
|
53 | if (!(this instanceof Router)) return new Router(opts);
|
54 |
|
55 | this.opts = opts;
|
56 | this.methods = this.opts.methods || [
|
57 | 'HEAD',
|
58 | 'OPTIONS',
|
59 | 'GET',
|
60 | 'PUT',
|
61 | 'PATCH',
|
62 | 'POST',
|
63 | 'DELETE'
|
64 | ];
|
65 | this.exclusive = Boolean(this.opts.exclusive);
|
66 |
|
67 | this.params = {};
|
68 | this.stack = [];
|
69 | this.host = this.opts.host;
|
70 | }
|
71 |
|
72 | /**
|
73 | * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such
|
74 | * as `router.get()` or `router.post()`.
|
75 | *
|
76 | * Match URL patterns to callback functions or controller actions using `router.verb()`,
|
77 | * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`.
|
78 | *
|
79 | * Additionally, `router.all()` can be used to match against all methods.
|
80 | *
|
81 | * ```javascript
|
82 | * router
|
83 | * .get('/', (ctx, next) => {
|
84 | * ctx.body = 'Hello World!';
|
85 | * })
|
86 | * .post('/users', (ctx, next) => {
|
87 | * // ...
|
88 | * })
|
89 | * .put('/users/:id', (ctx, next) => {
|
90 | * // ...
|
91 | * })
|
92 | * .del('/users/:id', (ctx, next) => {
|
93 | * // ...
|
94 | * })
|
95 | * .all('/users/:id', (ctx, next) => {
|
96 | * // ...
|
97 | * });
|
98 | * ```
|
99 | *
|
100 | * When a route is matched, its path is available at `ctx._matchedRoute` and if named,
|
101 | * the name is available at `ctx._matchedRouteName`
|
102 | *
|
103 | * Route paths will be translated to regular expressions using
|
104 | * [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
|
105 | *
|
106 | * Query strings will not be considered when matching requests.
|
107 | *
|
108 | * #### Named routes
|
109 | *
|
110 | * Routes can optionally have names. This allows generation of URLs and easy
|
111 | * renaming of URLs during development.
|
112 | *
|
113 | * ```javascript
|
114 | * router.get('user', '/users/:id', (ctx, next) => {
|
115 | * // ...
|
116 | * });
|
117 | *
|
118 | * router.url('user', 3);
|
119 | * // => "/users/3"
|
120 | * ```
|
121 | *
|
122 | * #### Multiple middleware
|
123 | *
|
124 | * Multiple middleware may be given:
|
125 | *
|
126 | * ```javascript
|
127 | * router.get(
|
128 | * '/users/:id',
|
129 | * (ctx, next) => {
|
130 | * return User.findOne(ctx.params.id).then(function(user) {
|
131 | * ctx.user = user;
|
132 | * next();
|
133 | * });
|
134 | * },
|
135 | * ctx => {
|
136 | * console.log(ctx.user);
|
137 | * // => { id: 17, name: "Alex" }
|
138 | * }
|
139 | * );
|
140 | * ```
|
141 | *
|
142 | * ### Nested routers
|
143 | *
|
144 | * Nesting routers is supported:
|
145 | *
|
146 | * ```javascript
|
147 | * const forums = new Router();
|
148 | * const posts = new Router();
|
149 | *
|
150 | * posts.get('/', (ctx, next) => {...});
|
151 | * posts.get('/:pid', (ctx, next) => {...});
|
152 | * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
|
153 | *
|
154 | * // responds to "/forums/123/posts" and "/forums/123/posts/123"
|
155 | * app.use(forums.routes());
|
156 | * ```
|
157 | *
|
158 | * #### Router prefixes
|
159 | *
|
160 | * Route paths can be prefixed at the router level:
|
161 | *
|
162 | * ```javascript
|
163 | * const router = new Router({
|
164 | * prefix: '/users'
|
165 | * });
|
166 | *
|
167 | * router.get('/', ...); // responds to "/users"
|
168 | * router.get('/:id', ...); // responds to "/users/:id"
|
169 | * ```
|
170 | *
|
171 | * #### URL parameters
|
172 | *
|
173 | * Named route parameters are captured and added to `ctx.params`.
|
174 | *
|
175 | * ```javascript
|
176 | * router.get('/:category/:title', (ctx, next) => {
|
177 | * console.log(ctx.params);
|
178 | * // => { category: 'programming', title: 'how-to-node' }
|
179 | * });
|
180 | * ```
|
181 | *
|
182 | * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is
|
183 | * used to convert paths to regular expressions.
|
184 | *
|
185 | *
|
186 | * ### Match host for each router instance
|
187 | *
|
188 | * ```javascript
|
189 | * const router = new Router({
|
190 | * host: 'example.domain' // only match if request host exactly equal `example.domain`
|
191 | * });
|
192 | *
|
193 | * ```
|
194 | *
|
195 | * OR host cloud be a regexp
|
196 | *
|
197 | * ```javascript
|
198 | * const router = new Router({
|
199 | * host: /.*\.?example\.domain$/ // all host end with .example.domain would be matched
|
200 | * });
|
201 | * ```
|
202 | *
|
203 | * @name get|put|post|patch|delete|del
|
204 | * @memberof module:koa-router.prototype
|
205 | * @param {String} path
|
206 | * @param {Function=} middleware route middleware(s)
|
207 | * @param {Function} callback route callback
|
208 | * @returns {Router}
|
209 | */
|
210 |
|
211 | for (const method_ of methods) {
|
212 | function setMethodVerb(method) {
|
213 | Router.prototype[method] = function (name, path, middleware) {
|
214 | if (typeof path === 'string' || path instanceof RegExp) {
|
215 | middleware = Array.prototype.slice.call(arguments, 2);
|
216 | } else {
|
217 | middleware = Array.prototype.slice.call(arguments, 1);
|
218 | path = name;
|
219 | name = null;
|
220 | }
|
221 |
|
222 | // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
|
223 | if (
|
224 | typeof path !== 'string' &&
|
225 | !(path instanceof RegExp) &&
|
226 | (!Array.isArray(path) || path.length === 0)
|
227 | )
|
228 | throw new Error(
|
229 | `You have to provide a path when adding a ${method} handler`
|
230 | );
|
231 |
|
232 | this.register(path, [method], middleware, { name });
|
233 |
|
234 | return this;
|
235 | };
|
236 | }
|
237 |
|
238 | setMethodVerb(method_);
|
239 | }
|
240 |
|
241 | // Alias for `router.delete()` because delete is a reserved word
|
242 | // eslint-disable-next-line dot-notation
|
243 | Router.prototype.del = Router.prototype['delete'];
|
244 |
|
245 | /**
|
246 | * Use given middleware.
|
247 | *
|
248 | * Middleware run in the order they are defined by `.use()`. They are invoked
|
249 | * sequentially, requests start at the first middleware and work their way
|
250 | * "down" the middleware stack.
|
251 | *
|
252 | * @example
|
253 | *
|
254 | * ```javascript
|
255 | * // session middleware will run before authorize
|
256 | * router
|
257 | * .use(session())
|
258 | * .use(authorize());
|
259 | *
|
260 | * // use middleware only with given path
|
261 | * router.use('/users', userAuth());
|
262 | *
|
263 | * // or with an array of paths
|
264 | * router.use(['/users', '/admin'], userAuth());
|
265 | *
|
266 | * app.use(router.routes());
|
267 | * ```
|
268 | *
|
269 | * @param {String=} path
|
270 | * @param {Function} middleware
|
271 | * @param {Function=} ...
|
272 | * @returns {Router}
|
273 | */
|
274 |
|
275 | Router.prototype.use = function () {
|
276 | const router = this;
|
277 | const middleware = Array.prototype.slice.call(arguments);
|
278 | let path;
|
279 |
|
280 | // support array of paths
|
281 | if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
|
282 | const arrPaths = middleware[0];
|
283 | for (const p of arrPaths) {
|
284 | router.use.apply(router, [p].concat(middleware.slice(1)));
|
285 | }
|
286 |
|
287 | return this;
|
288 | }
|
289 |
|
290 | const hasPath = typeof middleware[0] === 'string';
|
291 | if (hasPath) path = middleware.shift();
|
292 |
|
293 | for (const m of middleware) {
|
294 | if (m.router) {
|
295 | const cloneRouter = Object.assign(
|
296 | Object.create(Router.prototype),
|
297 | m.router,
|
298 | {
|
299 | stack: [...m.router.stack]
|
300 | }
|
301 | );
|
302 |
|
303 | for (let j = 0; j < cloneRouter.stack.length; j++) {
|
304 | const nestedLayer = cloneRouter.stack[j];
|
305 | const cloneLayer = Object.assign(
|
306 | Object.create(Layer.prototype),
|
307 | nestedLayer
|
308 | );
|
309 |
|
310 | if (path) cloneLayer.setPrefix(path);
|
311 | if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
|
312 | router.stack.push(cloneLayer);
|
313 | cloneRouter.stack[j] = cloneLayer;
|
314 | }
|
315 |
|
316 | if (router.params) {
|
317 | function setRouterParams(paramArr) {
|
318 | const routerParams = paramArr;
|
319 | for (const key of routerParams) {
|
320 | cloneRouter.param(key, router.params[key]);
|
321 | }
|
322 | }
|
323 |
|
324 | setRouterParams(Object.keys(router.params));
|
325 | }
|
326 | } else {
|
327 | const keys = [];
|
328 | pathToRegexp(router.opts.prefix || '', keys);
|
329 | const routerPrefixHasParam = router.opts.prefix && keys.length;
|
330 | router.register(path || '([^/]*)', [], m, {
|
331 | end: false,
|
332 | ignoreCaptures: !hasPath && !routerPrefixHasParam
|
333 | });
|
334 | }
|
335 | }
|
336 |
|
337 | return this;
|
338 | };
|
339 |
|
340 | /**
|
341 | * Set the path prefix for a Router instance that was already initialized.
|
342 | *
|
343 | * @example
|
344 | *
|
345 | * ```javascript
|
346 | * router.prefix('/things/:thing_id')
|
347 | * ```
|
348 | *
|
349 | * @param {String} prefix
|
350 | * @returns {Router}
|
351 | */
|
352 |
|
353 | Router.prototype.prefix = function (prefix) {
|
354 | prefix = prefix.replace(/\/$/, '');
|
355 |
|
356 | this.opts.prefix = prefix;
|
357 |
|
358 | for (let i = 0; i < this.stack.length; i++) {
|
359 | const route = this.stack[i];
|
360 | route.setPrefix(prefix);
|
361 | }
|
362 |
|
363 | return this;
|
364 | };
|
365 |
|
366 | /**
|
367 | * Returns router middleware which dispatches a route matching the request.
|
368 | *
|
369 | * @returns {Function}
|
370 | */
|
371 |
|
372 | Router.prototype.routes = Router.prototype.middleware = function () {
|
373 | const router = this;
|
374 |
|
375 | const dispatch = function dispatch(ctx, next) {
|
376 | debug('%s %s', ctx.method, ctx.path);
|
377 |
|
378 | const hostMatched = router.matchHost(ctx.host);
|
379 |
|
380 | if (!hostMatched) {
|
381 | return next();
|
382 | }
|
383 |
|
384 | const path =
|
385 | router.opts.routerPath || ctx.newRouterPath || ctx.path || ctx.routerPath;
|
386 | const matched = router.match(path, ctx.method);
|
387 | let layerChain;
|
388 |
|
389 | if (ctx.matched) {
|
390 | ctx.matched.push.apply(ctx.matched, matched.path);
|
391 | } else {
|
392 | ctx.matched = matched.path;
|
393 | }
|
394 |
|
395 | ctx.router = router;
|
396 |
|
397 | if (!matched.route) return next();
|
398 |
|
399 | const matchedLayers = matched.pathAndMethod;
|
400 | const mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
|
401 | ctx._matchedRoute = mostSpecificLayer.path;
|
402 | if (mostSpecificLayer.name) {
|
403 | ctx._matchedRouteName = mostSpecificLayer.name;
|
404 | }
|
405 |
|
406 | layerChain = (
|
407 | router.exclusive ? [mostSpecificLayer] : matchedLayers
|
408 | ).reduce(function (memo, layer) {
|
409 | memo.push(function (ctx, next) {
|
410 | ctx.captures = layer.captures(path, ctx.captures);
|
411 | ctx.params = ctx.request.params = layer.params(
|
412 | path,
|
413 | ctx.captures,
|
414 | ctx.params
|
415 | );
|
416 | ctx.routerPath = layer.path;
|
417 | ctx.routerName = layer.name;
|
418 | ctx._matchedRoute = layer.path;
|
419 | if (layer.name) {
|
420 | ctx._matchedRouteName = layer.name;
|
421 | }
|
422 |
|
423 | return next();
|
424 | });
|
425 | return memo.concat(layer.stack);
|
426 | }, []);
|
427 |
|
428 | return compose(layerChain)(ctx, next);
|
429 | };
|
430 |
|
431 | dispatch.router = this;
|
432 |
|
433 | return dispatch;
|
434 | };
|
435 |
|
436 | /**
|
437 | * Returns separate middleware for responding to `OPTIONS` requests with
|
438 | * an `Allow` header containing the allowed methods, as well as responding
|
439 | * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
|
440 | *
|
441 | * @example
|
442 | *
|
443 | * ```javascript
|
444 | * const Koa = require('koa');
|
445 | * const Router = require('@koa/router');
|
446 | *
|
447 | * const app = new Koa();
|
448 | * const router = new Router();
|
449 | *
|
450 | * app.use(router.routes());
|
451 | * app.use(router.allowedMethods());
|
452 | * ```
|
453 | *
|
454 | * **Example with [Boom](https://github.com/hapijs/boom)**
|
455 | *
|
456 | * ```javascript
|
457 | * const Koa = require('koa');
|
458 | * const Router = require('@koa/router');
|
459 | * const Boom = require('boom');
|
460 | *
|
461 | * const app = new Koa();
|
462 | * const router = new Router();
|
463 | *
|
464 | * app.use(router.routes());
|
465 | * app.use(router.allowedMethods({
|
466 | * throw: true,
|
467 | * notImplemented: () => new Boom.notImplemented(),
|
468 | * methodNotAllowed: () => new Boom.methodNotAllowed()
|
469 | * }));
|
470 | * ```
|
471 | *
|
472 | * @param {Object=} options
|
473 | * @param {Boolean=} options.throw throw error instead of setting status and header
|
474 | * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error
|
475 | * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error
|
476 | * @returns {Function}
|
477 | */
|
478 |
|
479 | Router.prototype.allowedMethods = function (options = {}) {
|
480 | const implemented = this.methods;
|
481 |
|
482 | return function allowedMethods(ctx, next) {
|
483 | return next().then(function () {
|
484 | const allowed = {};
|
485 |
|
486 | if (!ctx.status || ctx.status === 404) {
|
487 | for (let i = 0; i < ctx.matched.length; i++) {
|
488 | const route = ctx.matched[i];
|
489 | for (let j = 0; j < route.methods.length; j++) {
|
490 | const method = route.methods[j];
|
491 | allowed[method] = method;
|
492 | }
|
493 | }
|
494 |
|
495 | const allowedArr = Object.keys(allowed);
|
496 |
|
497 | if (!~implemented.indexOf(ctx.method)) {
|
498 | if (options.throw) {
|
499 | const notImplementedThrowable =
|
500 | typeof options.notImplemented === 'function'
|
501 | ? options.notImplemented() // set whatever the user returns from their function
|
502 | : new HttpError.NotImplemented();
|
503 |
|
504 | throw notImplementedThrowable;
|
505 | } else {
|
506 | ctx.status = 501;
|
507 | ctx.set('Allow', allowedArr.join(', '));
|
508 | }
|
509 | } else if (allowedArr.length > 0) {
|
510 | if (ctx.method === 'OPTIONS') {
|
511 | ctx.status = 200;
|
512 | ctx.body = '';
|
513 | ctx.set('Allow', allowedArr.join(', '));
|
514 | } else if (!allowed[ctx.method]) {
|
515 | if (options.throw) {
|
516 | const notAllowedThrowable =
|
517 | typeof options.methodNotAllowed === 'function'
|
518 | ? options.methodNotAllowed() // set whatever the user returns from their function
|
519 | : new HttpError.MethodNotAllowed();
|
520 |
|
521 | throw notAllowedThrowable;
|
522 | } else {
|
523 | ctx.status = 405;
|
524 | ctx.set('Allow', allowedArr.join(', '));
|
525 | }
|
526 | }
|
527 | }
|
528 | }
|
529 | });
|
530 | };
|
531 | };
|
532 |
|
533 | /**
|
534 | * Register route with all methods.
|
535 | *
|
536 | * @param {String} name Optional.
|
537 | * @param {String} path
|
538 | * @param {Function=} middleware You may also pass multiple middleware.
|
539 | * @param {Function} callback
|
540 | * @returns {Router}
|
541 | */
|
542 |
|
543 | Router.prototype.all = function (name, path, middleware) {
|
544 | if (typeof path === 'string') {
|
545 | middleware = Array.prototype.slice.call(arguments, 2);
|
546 | } else {
|
547 | middleware = Array.prototype.slice.call(arguments, 1);
|
548 | path = name;
|
549 | name = null;
|
550 | }
|
551 |
|
552 | // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
|
553 | if (
|
554 | typeof path !== 'string' &&
|
555 | !(path instanceof RegExp) &&
|
556 | (!Array.isArray(path) || path.length === 0)
|
557 | )
|
558 | throw new Error('You have to provide a path when adding an all handler');
|
559 |
|
560 | this.register(path, methods, middleware, { name });
|
561 |
|
562 | return this;
|
563 | };
|
564 |
|
565 | /**
|
566 | * Redirect `source` to `destination` URL with optional 30x status `code`.
|
567 | *
|
568 | * Both `source` and `destination` can be route names.
|
569 | *
|
570 | * ```javascript
|
571 | * router.redirect('/login', 'sign-in');
|
572 | * ```
|
573 | *
|
574 | * This is equivalent to:
|
575 | *
|
576 | * ```javascript
|
577 | * router.all('/login', ctx => {
|
578 | * ctx.redirect('/sign-in');
|
579 | * ctx.status = 301;
|
580 | * });
|
581 | * ```
|
582 | *
|
583 | * @param {String} source URL or route name.
|
584 | * @param {String} destination URL or route name.
|
585 | * @param {Number=} code HTTP status code (default: 301).
|
586 | * @returns {Router}
|
587 | */
|
588 |
|
589 | Router.prototype.redirect = function (source, destination, code) {
|
590 | // lookup source route by name
|
591 | if (typeof source === 'symbol' || source[0] !== '/') {
|
592 | source = this.url(source);
|
593 | if (source instanceof Error) throw source;
|
594 | }
|
595 |
|
596 | // lookup destination route by name
|
597 | if (
|
598 | typeof destination === 'symbol' ||
|
599 | (destination[0] !== '/' && !destination.includes('://'))
|
600 | ) {
|
601 | destination = this.url(destination);
|
602 | if (destination instanceof Error) throw destination;
|
603 | }
|
604 |
|
605 | return this.all(source, (ctx) => {
|
606 | ctx.redirect(destination);
|
607 | ctx.status = code || 301;
|
608 | });
|
609 | };
|
610 |
|
611 | /**
|
612 | * Create and register a route.
|
613 | *
|
614 | * @param {String} path Path string.
|
615 | * @param {Array.<String>} methods Array of HTTP verbs.
|
616 | * @param {Function} middleware Multiple middleware also accepted.
|
617 | * @returns {Layer}
|
618 | * @private
|
619 | */
|
620 |
|
621 | Router.prototype.register = function (path, methods, middleware, opts = {}) {
|
622 | const router = this;
|
623 | const { stack } = this;
|
624 |
|
625 | // support array of paths
|
626 | if (Array.isArray(path)) {
|
627 | for (const curPath of path) {
|
628 | router.register.call(router, curPath, methods, middleware, opts);
|
629 | }
|
630 |
|
631 | return this;
|
632 | }
|
633 |
|
634 | // create route
|
635 | const route = new Layer(path, methods, middleware, {
|
636 | end: opts.end === false ? opts.end : true,
|
637 | name: opts.name,
|
638 | sensitive: opts.sensitive || this.opts.sensitive || false,
|
639 | strict: opts.strict || this.opts.strict || false,
|
640 | prefix: opts.prefix || this.opts.prefix || '',
|
641 | ignoreCaptures: opts.ignoreCaptures
|
642 | });
|
643 |
|
644 | if (this.opts.prefix) {
|
645 | route.setPrefix(this.opts.prefix);
|
646 | }
|
647 |
|
648 | // add parameter middleware
|
649 | for (let i = 0; i < Object.keys(this.params).length; i++) {
|
650 | const param = Object.keys(this.params)[i];
|
651 | route.param(param, this.params[param]);
|
652 | }
|
653 |
|
654 | stack.push(route);
|
655 |
|
656 | debug('defined route %s %s', route.methods, route.path);
|
657 |
|
658 | return route;
|
659 | };
|
660 |
|
661 | /**
|
662 | * Lookup route with given `name`.
|
663 | *
|
664 | * @param {String} name
|
665 | * @returns {Layer|false}
|
666 | */
|
667 |
|
668 | Router.prototype.route = function (name) {
|
669 | const routes = this.stack;
|
670 |
|
671 | for (let len = routes.length, i = 0; i < len; i++) {
|
672 | if (routes[i].name && routes[i].name === name) return routes[i];
|
673 | }
|
674 |
|
675 | return false;
|
676 | };
|
677 |
|
678 | /**
|
679 | * Generate URL for route. Takes a route name and map of named `params`.
|
680 | *
|
681 | * @example
|
682 | *
|
683 | * ```javascript
|
684 | * router.get('user', '/users/:id', (ctx, next) => {
|
685 | * // ...
|
686 | * });
|
687 | *
|
688 | * router.url('user', 3);
|
689 | * // => "/users/3"
|
690 | *
|
691 | * router.url('user', { id: 3 });
|
692 | * // => "/users/3"
|
693 | *
|
694 | * router.use((ctx, next) => {
|
695 | * // redirect to named route
|
696 | * ctx.redirect(ctx.router.url('sign-in'));
|
697 | * })
|
698 | *
|
699 | * router.url('user', { id: 3 }, { query: { limit: 1 } });
|
700 | * // => "/users/3?limit=1"
|
701 | *
|
702 | * router.url('user', { id: 3 }, { query: "limit=1" });
|
703 | * // => "/users/3?limit=1"
|
704 | * ```
|
705 | *
|
706 | * @param {String} name route name
|
707 | * @param {Object} params url parameters
|
708 | * @param {Object} [options] options parameter
|
709 | * @param {Object|String} [options.query] query options
|
710 | * @returns {String|Error}
|
711 | */
|
712 |
|
713 | Router.prototype.url = function (name, params) {
|
714 | const route = this.route(name);
|
715 |
|
716 | if (route) {
|
717 | const args = Array.prototype.slice.call(arguments, 1);
|
718 | return route.url.apply(route, args);
|
719 | }
|
720 |
|
721 | return new Error(`No route found for name: ${String(name)}`);
|
722 | };
|
723 |
|
724 | /**
|
725 | * Match given `path` and return corresponding routes.
|
726 | *
|
727 | * @param {String} path
|
728 | * @param {String} method
|
729 | * @returns {Object.<path, pathAndMethod>} returns layers that matched path and
|
730 | * path and method.
|
731 | * @private
|
732 | */
|
733 |
|
734 | Router.prototype.match = function (path, method) {
|
735 | const layers = this.stack;
|
736 | let layer;
|
737 | const matched = {
|
738 | path: [],
|
739 | pathAndMethod: [],
|
740 | route: false
|
741 | };
|
742 |
|
743 | for (let len = layers.length, i = 0; i < len; i++) {
|
744 | layer = layers[i];
|
745 |
|
746 | debug('test %s %s', layer.path, layer.regexp);
|
747 |
|
748 | // eslint-disable-next-line unicorn/prefer-regexp-test
|
749 | if (layer.match(path)) {
|
750 | matched.path.push(layer);
|
751 |
|
752 | if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
|
753 | matched.pathAndMethod.push(layer);
|
754 | if (layer.methods.length > 0) matched.route = true;
|
755 | }
|
756 | }
|
757 | }
|
758 |
|
759 | return matched;
|
760 | };
|
761 |
|
762 | /**
|
763 | * Match given `input` to allowed host
|
764 | * @param {String} input
|
765 | * @returns {boolean}
|
766 | */
|
767 |
|
768 | Router.prototype.matchHost = function (input) {
|
769 | const { host } = this;
|
770 |
|
771 | if (!host) {
|
772 | return true;
|
773 | }
|
774 |
|
775 | if (!input) {
|
776 | return false;
|
777 | }
|
778 |
|
779 | if (typeof host === 'string') {
|
780 | return input === host;
|
781 | }
|
782 |
|
783 | if (typeof host === 'object' && host instanceof RegExp) {
|
784 | return host.test(input);
|
785 | }
|
786 | };
|
787 |
|
788 | /**
|
789 | * Run middleware for named route parameters. Useful for auto-loading or
|
790 | * validation.
|
791 | *
|
792 | * @example
|
793 | *
|
794 | * ```javascript
|
795 | * router
|
796 | * .param('user', (id, ctx, next) => {
|
797 | * ctx.user = users[id];
|
798 | * if (!ctx.user) return ctx.status = 404;
|
799 | * return next();
|
800 | * })
|
801 | * .get('/users/:user', ctx => {
|
802 | * ctx.body = ctx.user;
|
803 | * })
|
804 | * .get('/users/:user/friends', ctx => {
|
805 | * return ctx.user.getFriends().then(function(friends) {
|
806 | * ctx.body = friends;
|
807 | * });
|
808 | * })
|
809 | * // /users/3 => {"id": 3, "name": "Alex"}
|
810 | * // /users/3/friends => [{"id": 4, "name": "TJ"}]
|
811 | * ```
|
812 | *
|
813 | * @param {String} param
|
814 | * @param {Function} middleware
|
815 | * @returns {Router}
|
816 | */
|
817 |
|
818 | Router.prototype.param = function (param, middleware) {
|
819 | this.params[param] = middleware;
|
820 | for (let i = 0; i < this.stack.length; i++) {
|
821 | const route = this.stack[i];
|
822 | route.param(param, middleware);
|
823 | }
|
824 |
|
825 | return this;
|
826 | };
|
827 |
|
828 | /**
|
829 | * Generate URL from url pattern and given `params`.
|
830 | *
|
831 | * @example
|
832 | *
|
833 | * ```javascript
|
834 | * const url = Router.url('/users/:id', {id: 1});
|
835 | * // => "/users/1"
|
836 | * ```
|
837 | *
|
838 | * @param {String} path url pattern
|
839 | * @param {Object} params url parameters
|
840 | * @returns {String}
|
841 | */
|
842 | Router.url = function (path) {
|
843 | const args = Array.prototype.slice.call(arguments, 1);
|
844 | return Layer.prototype.url.apply({ path }, args);
|
845 | };
|