├── .gitignore ├── LICENSE ├── README.md ├── example ├── package.json └── server.js ├── index.js ├── package.json └── src ├── helpers.js ├── route.js ├── routeGroup.js ├── router.js └── urlBuilder.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hesam Mousavi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-router 2 | express advanced routing system 3 | 4 | > it`s next level express routing system 5 | 6 | ## Installation 7 | 8 | first of all you must install the package 9 | 10 | run this command first : 11 | ``` 12 | npm i express-router-js 13 | ``` 14 | 15 | ## How To Use 16 | 17 | after you install the package you can require express routing system like this : 18 | ```js 19 | 20 | const express = require('express') 21 | const app = express() 22 | const port = 3000 23 | const Router = require('express-router-js')(); 24 | 25 | Router.get('/' , (req , res , next) => { 26 | res.send('Hello World!'); 27 | }); 28 | 29 | Router.serve(app); 30 | 31 | app.listen(port, () => { 32 | console.log(`Example app listening on port ${port}`) 33 | }) 34 | ``` 35 | ## Define Route Group 36 | 37 | You Have Route Group in Express from now 38 | 39 | ```js 40 | 41 | const express = require('express') 42 | const app = express() 43 | const port = 3000 44 | const Router = require('express-router-js')() 45 | 46 | const AuthMiddleware = (req , res , next) => { 47 | // check user is login 48 | } 49 | 50 | const AdminMiddleware = = (req , res , next) => { 51 | // check user is admin 52 | } 53 | 54 | Router.group((Router) => { 55 | 56 | // localhost:3000/articles 57 | Router.get('/' , (req, res) => {}) 58 | 59 | // localhost:3000/articles/:articleId 60 | Router.get('/:articleId' , (req, res ) => {}) 61 | 62 | // localhost:3000/articles/:articleId 63 | Router.patch('/:articleId' , (req, res ) => {}) 64 | 65 | // localhost:3000/articles/:articleId/delete 66 | Router.delete('/:articleId' , (req, res ) => {}) 67 | 68 | }).prefix('articles').middleware([AuthMiddleware , AdminMiddleware]); 69 | 70 | 71 | Router.serve(app); 72 | 73 | app.listen(port, () => { 74 | console.log(`Example app listening on port ${port}`) 75 | }) 76 | ``` 77 | 78 | ## Add Middlewares to Route/Route-Group 79 | 80 | you can easily add your middlewares like this example : 81 | ```js 82 | Router.group((Router) => { 83 | Router.get('/' , (req, res) => {}).middleware(function(req , res , next) {}) 84 | }).middleware([function(req, res , next) {} , ...]) 85 | // Route group middleware will execute in All Route inside 86 | ``` 87 | 88 | ## Route Group in N-Level 89 | 90 | you can define diffrent route groups inside toghter 91 | 92 | ```js 93 | Router.group((Router) => { 94 | 95 | Router.group((Router) => { 96 | Router.get('/' , (req , res , next) => {}) 97 | }).prefix('videos'); 98 | 99 | Router.group((Router) => { 100 | Router.get('/' , (req , res , next) => {}) 101 | }).prefix('articles'); 102 | 103 | }).prefix('web').middleware([(req , res , next) => {} , ...]); 104 | ``` 105 | 106 | ## Naming Routes 107 | 108 | you can set names on routes to easy access them from request 109 | 110 | example: 111 | 112 | ```js 113 | 114 | Router.group((Router) => { 115 | 116 | // localhost:3000/user 117 | Router.get('/users' , (req, res) => { 118 | // this create an url to redirect 119 | let url = req.to_route('api.users.single' , { id : 2 }); 120 | 121 | }).as('users.list') 122 | 123 | // localhost:3000/user/:id 124 | Router.get('/user/:id' , (req, res) => { 125 | // this create an url to redirect 126 | let url = req.to_route('api.users'); 127 | 128 | }).as('users.single') 129 | 130 | }).prefix('api').as('api'); 131 | 132 | ``` 133 | 134 | 135 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.17.3", 15 | "express-router-js": "^0.0.10" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const port = 3000 4 | const Router = require('express-router-js')() 5 | 6 | const AuthMiddleware = (req , res , next) => { 7 | // check user is login 8 | console.log('auth') 9 | next(); 10 | } 11 | 12 | const AdminMiddleware = (req , res , next) => { 13 | // check user is admin 14 | console.log('admin') 15 | next(); 16 | } 17 | 18 | Router.group((Router) => { 19 | 20 | // localhost:3000/articles 21 | Router.get('/' , (req, res) => { 22 | return res.json({ articles : []}) 23 | }); 24 | 25 | // localhost:3000/articles/:articleId 26 | Router.get('/:articleId' , (req, res ) => {}) 27 | 28 | // localhost:3000/articles/:articleId 29 | Router.patch('/:articleId' , (req, res ) => {}) 30 | 31 | // localhost:3000/articles/:articleId/delete 32 | Router.delete('/:articleId' , (req, res ) => {}) 33 | 34 | }).prefix('articles').middleware([AuthMiddleware , AdminMiddleware]); 35 | 36 | Router.serve(app); 37 | 38 | app.listen(port, () => { 39 | console.log(`Example app listening on port ${port}`) 40 | }) 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Router = require('./src/router'); 2 | 3 | module.exports = () => Router.createRouter(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@express-router/express-router", 3 | "version": "1.2.4", 4 | "description": "express advanced routing system used adonis routing system", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "http://github.com/Hesammousavi/express-router-js" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "express", 15 | "routing", 16 | "route", 17 | "route-group" 18 | ], 19 | "dependencies": { 20 | "encodeurl": "^1.0.2", 21 | "qs": "^6.10.3" 22 | }, 23 | "author": "Hesam Mousavi", 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * orderd splashes 3 | * @param {string} input 4 | * @returns string 5 | */ 6 | function dropSlash(input) { 7 | if (input === '/') { 8 | return '/'; 9 | } 10 | return `/${input.replace(/^\//, '').replace(/\/$/, '')}`; 11 | } 12 | 13 | 14 | function normalizeMakeUrlOptions(params, options) { 15 | /** 16 | * Params used to be options earlier. So we are checking a few properties of it 17 | */ 18 | params = params || {}; 19 | options = options || {}; 20 | const normalizedParams = params['params'] ? params['params'] : params; 21 | const qs = options.qs || params['qs']; 22 | const domain = options.domain; 23 | const prefixUrl = options.prefixUrl; 24 | 25 | return { params: normalizedParams, qs, domain, prefixUrl }; 26 | } 27 | 28 | module.exports = { 29 | dropSlash, 30 | normalizeMakeUrlOptions 31 | } 32 | -------------------------------------------------------------------------------- /src/route.js: -------------------------------------------------------------------------------- 1 | const { dropSlash } = require('./helpers'); 2 | 3 | class Route { 4 | 5 | constructor (pattern, method, handler, globalMatchers) { 6 | this.pattern = pattern 7 | this.method = method 8 | this.handler = handler 9 | this.globalMatchers = globalMatchers 10 | 11 | /** 12 | * By default the route is part of `root` domain. Root 13 | * domain is used when no domain is defined 14 | */ 15 | this.routeDomain = 'root' 16 | 17 | /** 18 | * An object of matchers to be forwarded to the 19 | * store. The matchers list is populated by 20 | * calling `where` method 21 | */ 22 | this.matchers = {} 23 | 24 | /** 25 | * Custom prefixes. Usually added to a group of routes. We keep an array of them 26 | * since nested groups will want all of them ot concat. 27 | */ 28 | this.prefixes = [] 29 | 30 | /** 31 | * An array of middleware. Added using `middleware` function 32 | */ 33 | this.routeMiddleware = [] 34 | 35 | /** 36 | * An array of middleware. Added dont using `middleware` function 37 | */ 38 | this.routeWithoutMiddleware = [] 39 | } 40 | 41 | /** 42 | * Returns an object of param matchers by merging global and local 43 | * matchers. The local copy is given preference over the global 44 | * one's 45 | */ 46 | getMatchers () { 47 | return Object.assign({}, this.globalMatchers, this.matchers) 48 | } 49 | 50 | /** 51 | * Returns a normalized pattern string by prefixing the `prefix` (if defined). 52 | */ 53 | getPattern () { 54 | const pattern = dropSlash(this.pattern) 55 | const prefix = this.prefixes 56 | .slice() 57 | .reverse() 58 | .map((one) => dropSlash(one)) 59 | .join('') 60 | 61 | return prefix ? `${prefix}${pattern === '/' ? '' : pattern}` : pattern 62 | } 63 | 64 | getHandler () { 65 | let handler = this.handler; 66 | 67 | if (typeof handler == 'string' || handler instanceof String) { 68 | 69 | /** 70 | * the string must be this format 'ModuleController@method' 71 | * [0] => ModuleController 72 | * [1] => method 73 | * @type {string[]} 74 | */ 75 | let action = handler.split('@'); 76 | 77 | if(action.length !== 2) { 78 | throw new Error('handler must be this format destinationController@method') 79 | } 80 | 81 | // TODO -> define not modular @controller 82 | let pathOfModule = this.routeNamespace + '/' + action[0]; 83 | const controller = require(pathOfModule); 84 | 85 | handler = controller[action[1]]; 86 | } 87 | 88 | return handler; 89 | } 90 | 91 | getMiddlewares() { 92 | let withoutMiddlewares= this.routeWithoutMiddleware.flat() 93 | let middlewares = this.routeMiddleware.flat().filter(middleware => ! withoutMiddlewares.includes(middleware)) 94 | return middlewares; 95 | } 96 | 97 | /** 98 | * Define Regex matcher for a given param. If a matcher exists, then we do not 99 | * override that, since the routes inside a group will set matchers before 100 | * the group, so they should have priority over the route matchers. 101 | * 102 | * ``` 103 | * Route.group(() => { 104 | * Route.get('/:id', 'handler').where('id', /^[0-9]$/) 105 | * }).where('id', /[^a-z$]/) 106 | * ``` 107 | * 108 | * The `/^[0-9]$/` should win over the matcher defined by the group 109 | */ 110 | where (param, matcher) { 111 | if (this.matchers[param]) { 112 | return this 113 | } 114 | if (typeof matcher === 'string') { 115 | this.matchers[param] = { match: new RegExp(matcher) } 116 | } else if (helpers_1.types.isRegexp(matcher)) { 117 | this.matchers[param] = { match: matcher } 118 | } else { 119 | this.matchers[param] = matcher 120 | } 121 | return this 122 | } 123 | 124 | /** 125 | * Define prefix for the route. Prefixes will be concated 126 | * This method is mainly exposed for the [[RouteGroup]] 127 | */ 128 | prefix (prefix) { 129 | this.prefixes.push(prefix) 130 | return this 131 | } 132 | 133 | /** 134 | * Define a custom domain for the route. Again we do not overwrite the domain 135 | * unless `overwrite` flag is set to true. 136 | * 137 | * This is again done to make route.domain win over route.group.domain 138 | */ 139 | domain (domain, overwrite = false) { 140 | if (this.routeDomain === 'root' || overwrite) { 141 | this.routeDomain = domain 142 | } 143 | return this 144 | } 145 | 146 | /** 147 | * Define an array of middleware to be executed on the route. If `prepend` 148 | * is true, then middleware will be added to start of the existing 149 | * middleware. The option is exposed for [[RouteGroup]] 150 | */ 151 | middleware (middleware, prepend = false) { 152 | middleware = Array.isArray(middleware) ? middleware : [middleware] 153 | if (prepend) { 154 | this.routeMiddleware.unshift(middleware) 155 | } else { 156 | this.routeMiddleware.push(middleware) 157 | } 158 | 159 | return this 160 | } 161 | 162 | withoutMiddleware(middleware) { 163 | middleware = Array.isArray(middleware) ? middleware : [middleware] 164 | this.routeWithoutMiddleware.push(middleware) 165 | return this 166 | } 167 | 168 | /** 169 | * Give memorizable name to the route. This is helpful, when you 170 | * want to lookup route defination by it's name. 171 | * 172 | * If `prepend` is true, then it will keep on prepending to the existing 173 | * name. This option is exposed for [[RouteGroup]] 174 | */ 175 | as (name, prepend = false) { 176 | this.name = prepend ? `${name}.${this.name}` : name 177 | return this 178 | } 179 | 180 | /** 181 | * Define controller namespace for a given route 182 | */ 183 | namespace (namespace, overwrite = false) { 184 | if (!this.routeNamespace || overwrite) { 185 | this.routeNamespace = namespace 186 | } 187 | return this 188 | } 189 | 190 | /** 191 | * Returns [[RouteDefinition]] that can be passed to the [[Store]] for 192 | * registering the route 193 | */ 194 | toJSON () { 195 | return { 196 | domain: this.routeDomain, 197 | pattern: this.getPattern(), 198 | matchers: this.getMatchers(), 199 | meta: { 200 | namespace: this.routeNamespace, 201 | }, 202 | name: this.name, 203 | handler: this.getHandler(), 204 | method: this.method, 205 | middleware: this.getMiddlewares(), 206 | } 207 | } 208 | } 209 | 210 | module.exports = Route 211 | -------------------------------------------------------------------------------- /src/routeGroup.js: -------------------------------------------------------------------------------- 1 | class RouteGroup { 2 | constructor (routes) { 3 | // super(); 4 | this.routes = routes 5 | /** 6 | * Array of middleware registered on the group 7 | */ 8 | this.groupMiddleware = [] 9 | 10 | /** 11 | * Array of middleware not must registered on the group 12 | */ 13 | this.withoutGroupMiddleware = [] 14 | 15 | /** 16 | * We register the group middleware only once with the route 17 | * and then mutate the internal stack. This ensures that 18 | * group own middleware are pushed to the last, but the 19 | * entire group of middleware is added to the front 20 | * in the routes 21 | */ 22 | this.registeredMiddlewareWithRoute = false 23 | } 24 | 25 | /** 26 | * Invokes a given method with params on the route instance or route 27 | * resource instance 28 | */ 29 | invoke (route, method, params) { 30 | // if (route instanceof Resource_1.RouteResource) { 31 | // route.routes.forEach((child) => this.invoke(child, method, params)); 32 | // return; 33 | // } 34 | if (route instanceof RouteGroup) { 35 | route.routes.forEach((child) => this.invoke(child, method, params)) 36 | return 37 | } 38 | // if (route instanceof BriskRoute_1.BriskRoute) { 39 | // /* istanbul ignore else */ 40 | // if (route.route) { 41 | // /* 42 | // * Raise error when trying to prefix route name but route doesn't have 43 | // * a name 44 | // */ 45 | // if (method === 'as' && !route.route.name) { 46 | // throw RouterException_1.RouterException.cannotDefineGroupName(); 47 | // } 48 | // route.route[method](...params); 49 | // } 50 | // return; 51 | // } 52 | /* 53 | * Raise error when trying to prefix route name but route doesn't have 54 | * a name 55 | */ 56 | // if (method === 'as' && !route.name) { 57 | // throw RouterException_1.RouterException.cannotDefineGroupName(); 58 | // } 59 | route[method](...params) 60 | } 61 | 62 | /** 63 | * Define Regex matchers for a given param for all the routes. 64 | * 65 | * @example 66 | * ```ts 67 | * Route.group(() => { 68 | * }).where('id', /^[0-9]+/) 69 | * ``` 70 | */ 71 | where (param, matcher) { 72 | this.routes.forEach((route) => this.invoke(route, 'where', [param, matcher])) 73 | return this 74 | } 75 | 76 | /** 77 | * Define prefix all the routes in the group. 78 | * 79 | * @example 80 | * ```ts 81 | * Route.group(() => { 82 | * }).prefix('v1') 83 | * ``` 84 | */ 85 | prefix (prefix) { 86 | this.routes.forEach((route) => this.invoke(route, 'prefix', [prefix])) 87 | return this 88 | } 89 | 90 | /** 91 | * Define domain for all the routes. 92 | * 93 | * @example 94 | * ```ts 95 | * Route.group(() => { 96 | * }).domain('') 97 | * ``` 98 | */ 99 | domain (domain) { 100 | this.routes.forEach((route) => this.invoke(route, 'domain', [domain])) 101 | return this 102 | } 103 | 104 | /** 105 | * Prepend name to the routes name. 106 | * 107 | * @example 108 | * ```ts 109 | * Route.group(() => { 110 | * }).as('version1') 111 | * ``` 112 | */ 113 | as (name) { 114 | this.routes.forEach((route) => this.invoke(route, 'as', [name, true])) 115 | return this 116 | } 117 | 118 | /** 119 | * Prepend an array of middleware to all routes middleware. 120 | * 121 | * @example 122 | * ```ts 123 | * Route.group(() => { 124 | * }).middleware(['auth']) 125 | * ``` 126 | */ 127 | middleware (middleware, prepend = false) { 128 | middleware = Array.isArray(middleware) ? middleware : [middleware] 129 | if (prepend) { 130 | middleware.forEach((one) => this.groupMiddleware.unshift(one)) 131 | } else { 132 | middleware.forEach((one) => this.groupMiddleware.push(one)) 133 | } 134 | if (!this.registeredMiddlewareWithRoute) { 135 | this.registeredMiddlewareWithRoute = true 136 | this.routes.forEach((route) => this.invoke(route, 'middleware', [this.groupMiddleware, true])) 137 | } 138 | return this 139 | } 140 | 141 | withoutMiddleware(middleware) { 142 | middleware = Array.isArray(middleware) ? middleware : [middleware] 143 | middleware.forEach((one) => this.withoutGroupMiddleware.push(one)) 144 | 145 | this.routes.forEach((route) => this.invoke(route, 'withoutMiddleware', [this.withoutGroupMiddleware])) 146 | 147 | return this 148 | } 149 | 150 | 151 | /** 152 | * Define namespace for all the routes inside the group. 153 | * 154 | * @example 155 | * ```ts 156 | * Route.group(() => { 157 | * }).namespace('App/Admin/Controllers') 158 | * ``` 159 | */ 160 | namespace (namespace) { 161 | this.routes.forEach((route) => this.invoke(route, 'namespace', [namespace])) 162 | return this 163 | } 164 | } 165 | 166 | module.exports = RouteGroup; 167 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const Route = require('./route'); 2 | const RouteGroup = require('./routeGroup'); 3 | const UrlBuilder = require('./urlBuilder') 4 | const { normalizeMakeUrlOptions } = require('./helpers'); 5 | 6 | 7 | class Router { 8 | 9 | /** 10 | * all routes that user define push into routes property 11 | * @type {[ Route , RouteGroup ]} 12 | */ 13 | routes = [] 14 | 15 | /** 16 | * A boolean to tell the router that a group is in 17 | * open state right now 18 | */ 19 | openedGroups = [] 20 | 21 | app = null; 22 | 23 | /** 24 | * app an instance of expressjs app 25 | * @param app 26 | */ 27 | constructor () { 28 | } 29 | 30 | /** 31 | * set a Get Route 32 | * @param {string} uri 33 | * @param {function} action 34 | * @return Route 35 | * route.get(uri : string , action : function) : void 36 | */ 37 | get (uri, action) { 38 | return this.setRoute({ 39 | uri, 40 | action, 41 | method: 'get' 42 | }) 43 | } 44 | 45 | /** 46 | * set a Post Route 47 | * @param {string} uri 48 | * @param {function} action 49 | * @return Route 50 | * route.get(uri : string , action : function) : void 51 | */ 52 | post (uri, action) { 53 | return this.setRoute({ 54 | uri, 55 | action, 56 | method: 'post' 57 | }) 58 | } 59 | 60 | /** 61 | * set a Put Route 62 | * @param {string} uri 63 | * @param {function} action 64 | * @return Route 65 | * route.get(uri : string , action : function) : Route 66 | */ 67 | put (uri, action) { 68 | return this.setRoute({ 69 | uri, 70 | action, 71 | method: 'put' 72 | }) 73 | } 74 | 75 | /** 76 | * set a Patch Route 77 | * @param {string} uri 78 | * @param {function} action 79 | * @return Route 80 | * route.get(uri : string , action : function) : Route 81 | */ 82 | patch (uri, action) { 83 | return this.setRoute({ 84 | uri, 85 | action, 86 | method: 'patch' 87 | }) 88 | } 89 | 90 | /** 91 | * set a Delete Route 92 | * @param {string} uri 93 | * @param {function} action 94 | * @return Route 95 | * route.get(uri : string , action : function) : Route 96 | */ 97 | delete (uri, action) { 98 | return this.setRoute({ 99 | uri, 100 | action, 101 | method: 'delete' 102 | }) 103 | } 104 | 105 | 106 | getRecentGroup () { 107 | return this.openedGroups[this.openedGroups.length - 1] 108 | } 109 | 110 | /* 111 | * @param {string} uri 112 | * @param {function} action 113 | * @param {string} method 114 | */ 115 | setRoute ({ 116 | uri, 117 | action, 118 | method 119 | }) { 120 | 121 | const route = new Route(uri, method, action, {}) 122 | const openedGroup = this.getRecentGroup() 123 | 124 | if (openedGroup) { 125 | openedGroup.routes.push(route) 126 | } else { 127 | this.routes.push(route) 128 | } 129 | 130 | return route 131 | } 132 | 133 | group (callback) { 134 | /* 135 | * Create a new group with empty set of routes 136 | */ 137 | const group = new RouteGroup([]) 138 | /* 139 | * See if there is any opened existing route groups. If yes, then we 140 | * push this new group to the old group, otherwise we push it to 141 | * the list of routes. 142 | */ 143 | const openedGroup = this.getRecentGroup() 144 | if (openedGroup) { 145 | openedGroup.routes.push(group) 146 | } else { 147 | this.routes.push(group) 148 | } 149 | /* 150 | * Track the group, so that the upcoming calls inside the callback 151 | * can use this group 152 | */ 153 | this.openedGroups.push(group) 154 | /* 155 | * Execute the callback. Now all registered routes will be 156 | * collected seperately from the `routes` array 157 | */ 158 | callback(this) 159 | /* 160 | * Now the callback is over, get rid of the opened group 161 | */ 162 | this.openedGroups.pop() 163 | 164 | return group 165 | } 166 | 167 | serve (app) { 168 | this.app = app; 169 | 170 | let routes = this.toRoutesJSON(this.routes) 171 | 172 | // add route helpers for route handlers 173 | this.app.use(this.appendSomeRouteHelpersToRequest.bind(this)) 174 | 175 | for (const route of routes) { 176 | this.handleRoute(route) 177 | } 178 | } 179 | 180 | /** 181 | * Returns a flat list of routes JSON 182 | */ 183 | toRoutesJSON (routes) { 184 | return routes.reduce((list, route) => { 185 | 186 | if (route instanceof RouteGroup) { 187 | list = list.concat(this.toRoutesJSON(route.routes)) 188 | return list 189 | } 190 | 191 | list.push(route.toJSON()) 192 | 193 | return list 194 | }, []) 195 | } 196 | 197 | handleRoute (route) { 198 | this.app[route.method]( 199 | route.pattern, 200 | route.middleware, 201 | route.handler 202 | ) 203 | } 204 | 205 | static createRouter () { 206 | return new Router() 207 | } 208 | 209 | 210 | /** 211 | * Makes url to a registered route by looking it up with the route pattern, 212 | * name or the controller.method 213 | */ 214 | makeUrl(routeIdentifier, params, options) { 215 | const builder = new UrlBuilder({}, this.toRoutesJSON(this.routes) ); 216 | const normalizedOptions = normalizeMakeUrlOptions(params, options); 217 | 218 | normalizedOptions.params && builder.params(normalizedOptions.params); 219 | normalizedOptions.qs && builder.qs(normalizedOptions.qs); 220 | normalizedOptions.prefixUrl && builder.prefixUrl(normalizedOptions.prefixUrl); 221 | 222 | return builder.make(routeIdentifier); 223 | } 224 | 225 | appendSomeRouteHelpersToRequest (req , res , next) { 226 | req.to_route = this.makeUrl.bind(this); 227 | next(); 228 | } 229 | 230 | } 231 | 232 | module.exports = Router 233 | -------------------------------------------------------------------------------- /src/urlBuilder.js: -------------------------------------------------------------------------------- 1 | const qs = require("qs"); 2 | const encodeurl = require("encodeurl"); 3 | 4 | class UrlBuilder { 5 | constructor(encryption, routes) { 6 | this.encryption = encryption; 7 | this.routes = routes; 8 | /** 9 | * A custom query string to append to the URL 10 | */ 11 | this.queryString = {}; 12 | } 13 | /** 14 | * Processes the pattern against the params 15 | */ 16 | processPattern(pattern) { 17 | let url = []; 18 | const isParamsAnArray = Array.isArray(this.routeParams); 19 | /* 20 | * Split pattern when route has dynamic segments 21 | */ 22 | const tokens = pattern.split('/'); 23 | let paramsIndex = 0; 24 | for (const token of tokens) { 25 | 26 | /** 27 | * Token is a static value 28 | */ 29 | if (!token.startsWith(':')) { 30 | url.push(token); 31 | } 32 | else { 33 | const isOptional = token.endsWith('?'); 34 | const paramName = token.replace(/^:/, '').replace(/\?$/, ''); 35 | const param = isParamsAnArray ? this.routeParams[paramsIndex] : this.routeParams[paramName]; 36 | paramsIndex++; 37 | /* 38 | * A required param is always required to make the complete URL 39 | */ 40 | if (!param && !isOptional) { 41 | throw new Error('the routes needs params, set the params first'); 42 | } 43 | url.push(param); 44 | } 45 | } 46 | return url.join('/'); 47 | } 48 | /** 49 | * Finds the route inside the list of registered routes and 50 | * raises exception when unable to 51 | */ 52 | findRouteOrFail(identifier) { 53 | const route = this.routes.find(({ name, pattern, handler }) => { 54 | return name === identifier || pattern === identifier || handler === identifier; 55 | }); 56 | if (!route) { 57 | throw new Error('there is no route registered'); 58 | } 59 | return route; 60 | } 61 | /** 62 | * Suffix the query string to the URL 63 | */ 64 | suffixQueryString(url) { 65 | if (this.queryString) { 66 | const encoded = qs.stringify(this.queryString); 67 | url = encoded ? `${url}?${encodeurl(encoded)}` : url; 68 | } 69 | return url; 70 | } 71 | /** 72 | * Prefix a custom url to the final URI 73 | */ 74 | prefixUrl(url) { 75 | this.baseUrl = url; 76 | return this; 77 | } 78 | /** 79 | * Append query string to the final URI 80 | */ 81 | qs(queryString) { 82 | if (!queryString) { 83 | return this; 84 | } 85 | this.queryString = queryString; 86 | return this; 87 | } 88 | /** 89 | * Define required params to resolve the route 90 | */ 91 | params(params) { 92 | if (!params) { 93 | return this; 94 | } 95 | this.routeParams = params; 96 | return this; 97 | } 98 | /** 99 | * Generate url for the given route identifier 100 | */ 101 | make(identifier) { 102 | const route = this.findRouteOrFail(identifier); 103 | const url = this.processPattern(route.pattern); 104 | return this.suffixQueryString(this.baseUrl ? `${this.baseUrl}${url}` : url); 105 | } 106 | } 107 | 108 | 109 | module.exports = UrlBuilder; 110 | --------------------------------------------------------------------------------