diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9d2e029..0000000 --- a/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "presets": [ - "env", - "stage-0", - "stage-1", - "stage-2" - ], - "plugins": [] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3671463..41e25dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules /package-lock.json -*.log* \ No newline at end of file +*.log* +.nyc_output +.rpt2_cache \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..570a056 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +/src +/test +.nyc_output +.rpt2_cache +.travis.yml +.gitignore +rollup.config.js +tsconfig.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 7623345..b48ba78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: - - 7.10.0 - 8.0.0 - 9.1.0 \ No newline at end of file diff --git a/dist/index.min.js b/dist/index.min.js new file mode 100644 index 0000000..8e4b7d3 --- /dev/null +++ b/dist/index.min.js @@ -0,0 +1 @@ +"use strict";function sortRoutes(e){if(!Array.isArray(e))throw new Error("argument is expected to be an array of object, ["+typeof e+"] type is given.");return groupSortWildcards(e=groupBySlashes(e))}function groupSortWildcards(e){return Object.entries(e).map(function(e){e[0];return e[1].sort(wildCard)}).reduce(function(e,r){return r.concat(e)})}function groupBySlashes(e){return e.reduce(function(e,r){var t=r.match.split("/").length;return e[t]=e[t]||[],e[t].push(r),e},{})}function wildCard(e,r){var t=e.match.match(/\/(\:[a-zA-Z0-9_-]{1,})/g),n=r.match.match(/\/(\:[a-zA-Z0-9_-]{1,})/g),o=e.match.split("/"),a=r.match.split("/"),i=a?a.length:0;if(!t)return-1;if(!n)return 1;if(t.length===i-1)return 1;if(n.length===i-1)return-1;var u=t.map(function(e){return o.indexOf(e.replace("/",""))}).reduce(function(e,r){return(e+r)/o.length});return n.map(function(e){return a.indexOf(e.replace("/",""))}).reduce(function(e,r){return(e+r)/a.length})-u}function noop(e,r,t){}function normalizeEndpoint(e){return"/"+e.replace(/^\/|\/$/,"")}function routeTranslator(e,n,o){return Array.isArray(e)&&e.length?[].concat.apply([],e.map(function(e){var r=e.group&&e.resources,t=e.resources&&!e.controller;if(r)throw new Error("Group and Resources cannot co-exist.");if(t)throw new Error("Cannot resolve resources without specifying a controller");return e.resources?(console.log("This feature is under development"),routeTranslator(resolveResources(e),mapMiddleware([n,e.middleware]),o)):e.group?routeTranslator(e.routes,mapMiddleware([n,e.middleware]),appendPrefix(e.group,o)):routeTranslator(e,n,o)})):(n=mapMiddleware([n,e.middleware]),o=appendPrefix(e.match,o),{method:assignWithDefault(e.method,"get"),match:normalizeEndpoint(o),middleware:n,action:assignWithDefault(e.action,noop)})}function assignWithDefault(e,r){return e||r}function resolveResources(o){var a=[{method:"get",match:"/",functionName:"index"},{method:"get",match:"/:id",functionName:"view"},{method:"post",match:"/",functionName:"store"},{method:"put",match:"/:id",functionName:"update"},{method:"patch",match:"/:id",functionName:"patch"},{method:"del",match:"/:id",functionName:"delete"}];return Object.entries(o.controller).map(function(e){var r=e[0],t=e[1],n=a.find(function(e){return e.functionName===r});if(n)return{method:n.method,match:appendPrefix(n.match,o.resources),action:t}}).filter(function(e){return e})}function mapMiddleware(e){var r=[];return e.forEach(function(e){Array.isArray(e)?r.push.apply(r,e):r.push(e)}),r.filter(function(e){return e})}function appendPrefix(e,r){return void 0===r&&(r=!1),r&&(e=e.replace(/^\/|\/$/g,""),e=("/"+(r=r.replace(/^\/|\/$/g,""))+"/"+e).replace("//","/")),e.replace(/\/$/,"")}function groupMiddleware(e){var r={before:[],after:[]};return e.forEach(function(e){isValidArray(e)?isValidMiddlewarePosition(e[0])&&r[e[0]].push(e[1]):r.before.push(e)}),[r.before,r.after]}function isValidMiddlewarePosition(e){return"before"===e||"after"===e}function isValidArray(e){return Array.isArray(e)&&1 elem)\r\n\tprefix \t\t = prefix ? `${prefix}${route.match}`.replace('//', '/') : route.match\r\n\r\n\treturn {\r\n\t\tmethod: route.method,\r\n\t\tmatch: normalizeEndpoint(prefix),\r\n\t\tmiddleware,\r\n\t\taction: route.action\r\n\t}\r\n}\r\n\r\n/**\r\n * takes a restify server as an argument, returns a function\r\n * that takes an array of object.\r\n * @param {Server} server restify server\r\n * @returns routing function\r\n */\r\nexport default function configureRoutes(server) {\r\n\r\n\treturn function (routes) {\r\n\t\troutes = sortRoutes(\r\n\t\t\t[].concat( ...routeTranslator(routes) )\r\n\t\t)\r\n\r\n\t\t// safely route flatten translated routes.\r\n\t\treturn routes.map(function (route) {\r\n\t\t\tif (route.middleware.length) {\r\n\t\t\t\treturn server[route.method](\r\n\t\t\t\t\troute.match\r\n\t\t\t\t\t, ...route.middleware\r\n\t\t\t\t\t, route.action\r\n\t\t\t\t)\r\n\t\t\t}\r\n\r\n\t\t\treturn server[route.method](route.match, route.action)\r\n\t\t})\r\n\t}\r\n}\r\n\r\n" - ] -} diff --git a/dist/src/router.d.ts b/dist/src/router.d.ts new file mode 100644 index 0000000..3743a44 --- /dev/null +++ b/dist/src/router.d.ts @@ -0,0 +1,24 @@ +/** + * A recursive function that takes a required route as an argument, + * then calls itself if it's an array, otherwise returns the + * translated route. + * @param {mixed} route array or object containing the route information + * @param {func} middleware middleware of the endpoint/group + * @param {string} prefix prepended to the beginning of endpoint + * @returns object containing the translated route + */ +export declare function routeTranslator(route: any, middleware?: any, prefix?: any): any; +/** + * Flatten your route, takes care of your middleware and everything. + * @param routes + * @return Array + */ +export declare function transformRoutes(routes: any): any; +/** + * takes a restify server as an argument, returns a function + * that takes an array of object. + * @param {Server} server restify server + * @param {boolean} verbose log routing + * @returns routing function + */ +export default function configureRoutes(server: any, verbose?: boolean, logger?: (message?: any, ...optionalParams: any[]) => void): (routes: any) => void; diff --git a/dist/src/sorts.d.ts b/dist/src/sorts.d.ts new file mode 100644 index 0000000..456397a --- /dev/null +++ b/dist/src/sorts.d.ts @@ -0,0 +1,29 @@ +/** + * sorts the given routes + * @param {array} routes array of routes + * @returns sorted routes + */ +export declare function sortRoutes(routes: Array): any; +/** + * Group sorted routes based on wildcards + * @param routes + * @return grouped routes + */ +export declare function groupSortWildcards(routes: Array): any; +/** + * function for sorting based on slash count + * @param previous previous element + * @param current current element + */ +export declare function slashCount(previous: any, current: any): number; +/** + * Group routes based on its slash count + * @param array routes + */ +export declare function groupBySlashes(array: Array): any; +/** + * function for sorting based on wildcards + * @param previous previous element + * @param current current element + */ +export declare function wildCard(previous: any, current: any): number; diff --git a/dist/test/router.test.d.ts b/dist/test/router.test.d.ts new file mode 100644 index 0000000..509db18 --- /dev/null +++ b/dist/test/router.test.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/index.js b/index.js deleted file mode 100644 index eb060fc..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/router.js') \ No newline at end of file diff --git a/package.json b/package.json index 9b092ee..e783217 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,55 @@ { - "name": "restify-route-config", - "version": "0.1.0", - "description": "", - "main": "index.js", + "name": "restify-router-config", + "version": "1.2.0-alpha.0", + "description": "The laziest way to route your restify app.", + "main": "dist/index.min.js", + "types": "dist/src/router.d.ts", "scripts": { - "clean:assets": "rimraf ./public/assets", - "postinstall": "npm run compile", - "clean": "rm -rf dist && mkdir dist", - "clean:compile": "rm -rf dist/compiled", - "compile:babel": "babel-compile -p env src:dist", - "compile:minify": "minify dist/compiled -d dist", - "compile": "npm run clean && npm run compile:babel", - "dev": "nodemon server.js --exec babel-node --presets=env,stage-0,stage-1,stage-2 --watch", - "test": "_mocha test --require babel-polyfill --require babel-register" + "test": "nyc --check-coverage --lines 10 mocha -r ts-node/register test/**.test.ts", + "rollup": "rollup -c", + "clean": "rimraf ./dist", + "build": "npm run clean && npm run rollup" }, "keywords": [ "restify", + "restify-router", + "express", + "express-router-config", + "express-router", "router", "config", "react", "inspired" ], + "nyc": { + "include": [ + "src/**/*.ts" + ], + "extension": [ + ".ts", + ".tsx" + ], + "require": [ + "ts-node/register" + ] + }, "author": "", "license": "ISC", "devDependencies": { - "babel-cli": "^6.26.0", - "babel-compile": "^2.0.0", - "babel-core": "^6.26.0", - "babel-minify": "^0.2.0", - "babel-preset-env": "^1.6.1", - "babel-preset-stage-0": "^6.24.1", - "babel-preset-stage-1": "^6.24.1", - "babel-preset-stage-2": "^6.24.1", + "@types/chai": "^4.1.3", + "@types/mocha": "^5.2.0", + "@types/node": "^10.1.0", + "@types/restify": "^7.2.0", "chai": "^4.1.2", + "mocha": "^5.1.1", + "nyc": "^11.8.0", "rimraf": "^2.6.2", - "xo": "^0.18.2" + "rollup": "^0.58.2", + "rollup-plugin-alias": "^1.4.0", + "rollup-plugin-typescript2": "^0.14.0", + "rollup-plugin-uglify": "^4.0.0", + "ts-node": "^6.0.3", + "typescript": "^2.8.3", + "uglify-js": "^3.3.25" } } diff --git a/readme.md b/readme.md index 765e79b..f804a21 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,21 @@ -restify-route-config ---- +# restify-router-config +> The laziest way to route your restify app. + +[![Build Status][travis-image]][travis-url] +[![NPM Version][npm-image]][npm-url] +[![Downloads Stats][npm-downloads]][npm-url] + +This is an effort to maximize laziness in routing restify app. This tool includes grouping of routes, sorting of routes, and applying multiple middleware in most convenient way, of course the structure is inspired by [react-router-config](https://www.npmjs.com/package/react-router-config). + +NOTE: This module is also compatible with [express](https://expressjs.com/) but usage may vary. + +## Features: -react-router-config inspired Restify routing tool. meh... +* Sorting - will sort your routes, wildcards has been taken into account too. +* Grouping - will add a prefix to your routes depending on the group. +* Multiple Middleware - will let you add one or more middleware in most convenient way. +* Pre/Post Middleware - let's you define whether the middleware should execute before or after executing your function +* Nesting groups - allows you to nest your groups. ## Installation @@ -12,42 +26,194 @@ npm install -S restify-router-config ## Usage ```javascript -const restify = require('restify') -const router = require('restify-router-config') -const { getUserById, getUser, login } = require('./controllers/users') -const { restrictedRoute } = require('./middlewares') +import * as restify from 'restify' +import router from 'restify-router-config' +import { getUserById, getUser, login } from './controllers/users' +import { restrictedRoute } from './middlewares' /** create restify server */ const server = restify.createServer() /** configure your routes */ +// NOTE: you may or may not use group, but for the sake of grouping up +// your endpoints it's best to use it. router(server)([ - { - group: 'users', - routes: [ - { - match: '/:id', - middleware: restrictedRoute, - method: 'get', - action: getUserById - }, - { - match: '/', - method: 'get', - action: getUser - } - ] - }, - { - match: '/login', - method: 'post', - action: login - } + { + group: 'users', + routes: [ + { + match: '/:id', // would generate /users/:id route + middleware: restrictedRoute, + method: 'get', + action: getUserById + }, + { + match: '/', // would generate /users + method: 'get', + action: getUser + } + ] + }, + { + match: '/login', // would generate login + method: 'post', + action: login + } ]) server.listen(8080) ``` + +### Nesting groups + +```javascript +const router = require('restify-router-config') +const restify = require('restify') + +const apiAuth = (req, res, next) => { + console.log('authed!'); + next() +} + +const loggingMW = (req, res, next) => { + console.log(req._timeStart) + + next() +} + +const logDone = (req, res, next) => { + console.log('done!') + + next() +} + + +router(server) ([ + { + group: 'api/v1', + middleware: apiAuth, + routes: [ + { + match: '/hello', + method: 'get', + action: (req, res, next) => res.send('hello') + }, + { + group: 'users', + middleware: [ + ['before', loggingMW], + ['after', logDone] + ], + routes: [ + { + match: '/:id', + method: 'get', + action: (req, res, next) => { + res.send('hello') + + next() + } + }, + { + match: '/:id/friends', + method: 'get', + action: (req, res, next) => { + res.send('hello') + + next() + } + }, + { + match: '/', + method: 'get', + action: (req, res, next) => { + res.send('hello') + + next() + } + } + ] + } + ] + } +]) + +server.listen(4000) +``` +The example above would generate the following routes: +``` +[get] - /api/v1/users/:id/friends +[get] - /api/v1/users/:id +[get] - /api/v1/hello +[get] - /api/v1/users +``` + +### Setting middleware's execution + +You may choose to add middleware after the function's execution, in order to do that you must use the following syntax: + +```javascript +import * as restify from 'restify' +import router from 'restify-router-config' +import { getUserById } from './controllers/users' +import { authenticate, logger } from './middlewares' + +/** create restify server */ +const server = restify.createServer() +/** configure your routes */ +// NOTE: you may or may not use group, but for the sake of grouping up +// your endpoints it's best to use it. +router(server)([ + { + match: '/users/:id', + method: 'get', + middleware: [ + ['before', authenticate], + ['after', logger] + ] + action: getUserById + } +]) + +server.listen(8080) +``` +in the example above, the `/users/:id` route is authenticated first, then executes `getUserById`, after the execution of the `getUserById` the `logger` is executed right away. + -## Important Notice +## Why use restify-router-config? -* Didn't have time to write tests, so yea, don't expect that things should work the way it should. -* Current releases may not be stable until further notice. I didn't have much time to tinker around. Any kind of help is appreciated. \ No newline at end of file +You don't have to, but if you're used to `react-router-config`, then this would make it easier for you to configure your routes as +it is almost similar structure to `react-router-config`, another advantage of using this route tool is you can easily chain your middleware. For instance if you want to protect all of your routes, and at same time a single route would require additional middleware, you'd do it like this: + +```javascript +import { anotherMiddleware } from './middlewares' + +router(server)([ + { + // assuming that you want to protect all routes under /users + group: 'users', + middleware: restrictedRoute, + routes: [ + { + match: '/:id', + middleware: anotherMiddleware, + method: 'get', + action: getUserById + }, + { + match: '/', + method: 'get', + action: getUser + } + ] + } +]) +``` +the code from above would use `restrictedRoute` middleware first, and if you're going to access /users/:id, +it would also use `anotherMiddleware` middleware. + + +[npm-image]: https://img.shields.io/npm/v/restify-router-config.svg?style=flat-square +[npm-url]: https://npmjs.org/package/restify-router-config +[npm-downloads]: https://img.shields.io/npm/dm/restify-router-config.svg?style=flat-square +[travis-image]: https://travis-ci.org/yakovmeister/restify-router-config.svg?branch=dev +[travis-url]: https://travis-ci.org/yakovmeister/restify-router-config \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..4affe48 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,12 @@ +import typescript from 'rollup-plugin-typescript2' +import { uglify } from 'rollup-plugin-uglify' + +export default { + entry: 'src/router.ts', + dest: 'dist/index.min.js', + format: 'cjs', + plugins: [ + typescript(), + uglify() + ] +} diff --git a/src/router.js b/src/router.js deleted file mode 100644 index 15c4b69..0000000 --- a/src/router.js +++ /dev/null @@ -1,111 +0,0 @@ - -/** - * Sort string based on slash occurence and length of - * its last phrase. - * - * @param {string} previous previous sort object - * @param {string} current current sort object - * @returns new sort value - */ -function sortByNumberOfSlashes(previous, current) { - previous = previous.match.split('/') - current = current.match.split('/') - - const currentSlashCount = current.length - , previousSlashCount = previous.length - , currentLastPhrase = current[current.length - 1].length - , previousLastPhrase = previous[previous.length - 1].length - - if (currentSlashCount === previousSlashCount) { - return currentLastPhrase - previousLastPhrase - } - - return currentSlashCount - previousSlashCount -} - -/** - * sorts the given routes - * @param {array} routes array of routes - * @returns sorted routes - */ -function sortRoutes(routes) { - if (!Array.isArray(routes)) { - throw new Error(`routes are expected to be an array of object, ${typeof routes} given.`) - } - - return routes.sort(sortByNumberOfSlashes) -} - -/** - * normalize our endpoint, make sure it doesn't - * end with slash. - * @param {string} endpoint endpoint to be normalized - * @returns normalized endpoint - */ -function normalizeEndpoint(endpoint) { - return `/${endpoint.replace(/^\/|\/$/, '')}` -} - -/** - * A recursive function that takes a required route as an argument, - * then calls itself if it's an array, otherwise returns the - * translated route. - * @param {mixed} route array or object containing the route information - * @param {func} middleware middleware of the endpoint/group - * @param {string} prefix prepended to the beginning of endpoint - * @returns object containing the translated route - */ -function routeTranslator(route, middleware, prefix) { - if (Array.isArray(route)) { - return route.map(function (_route) { - if (_route.group) { - return routeTranslator( - _route.routes - , _route.middleware - , _route.group - ) - } - - return routeTranslator(_route, middleware, prefix) - }) - } - - middleware = [ middleware, route.middleware ].filter(elem => elem) - prefix = prefix ? `${prefix}${route.match}`.replace('//', '/') : route.match - - return { - method: route.method, - match: normalizeEndpoint(prefix), - middleware, - action: route.action - } -} - -/** - * takes a restify server as an argument, returns a function - * that takes an array of object. - * @param {Server} server restify server - * @returns routing function - */ -export default function configureRoutes(server) { - - return function (routes) { - routes = sortRoutes( - [].concat( ...routeTranslator(routes) ) - ) - - // safely route flatten translated routes. - return routes.map(function (route) { - if (route.middleware.length) { - return server[route.method]( - route.match - , ...route.middleware - , route.action - ) - } - - return server[route.method](route.match, route.action) - }) - } -} - diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..851949a --- /dev/null +++ b/src/router.ts @@ -0,0 +1,246 @@ +import { sortRoutes } from './sorts' + +/** + * Assign empty action if action was not specified + * @param req server request + * @param res server respones + * @param next ... + */ +function noop(req, res, next) { } + +/** + * normalize our endpoint, make sure it doesn't + * end with slash. + * @param {string} endpoint endpoint to be normalized + * @returns normalized endpoint + */ +function normalizeEndpoint(endpoint: string) { + return `/${endpoint.replace(/^\/|\/$/, '')}` +} + +/** + * A recursive function that takes a required route as an argument, + * then calls itself if it's an array, otherwise returns the + * translated route. + * @param {mixed} route array or object containing the route information + * @param {func} middleware middleware of the endpoint/group + * @param {string} prefix prepended to the beginning of endpoint + * @returns object containing the translated route + */ +export function routeTranslator(route : any, middleware ?: any, prefix ?: any) { + if (Array.isArray(route) && route.length) { + return [].concat(...route.map(function (_route) { + const groupAndResourcesCoExist = _route.group && _route.resources + const resourcesHasNoController = _route.resources && !_route.controller + + if (groupAndResourcesCoExist) { + throw new Error(`Group and Resources cannot co-exist.`) + } + + if (resourcesHasNoController) { + throw new Error(`Cannot resolve resources without specifying a controller`) + } + + if (_route.resources) { + console.log(`This feature is under development`) + + const resolved = resolveResources(_route) + + return routeTranslator( + resolved + , mapMiddleware([middleware, _route.middleware]) + , prefix + ) + } + + if (_route.group) { + return routeTranslator( + _route.routes + , mapMiddleware([middleware, _route.middleware]) + , appendPrefix(_route.group, prefix) + ) + } + + return routeTranslator(_route, middleware, prefix) + })) + } + + middleware = mapMiddleware([ middleware, route.middleware ]) + prefix = appendPrefix(route.match, prefix) + + return { + method: assignWithDefault(route.method, 'get'), + match: normalizeEndpoint(prefix), + middleware, + action: assignWithDefault(route.action, noop) + } +} + +/** + * Safely assign default value if first argument is undefined + * @param value value intended to be assigned + * @param defaultValue fallback value + * @return mixed + */ +function assignWithDefault(value, defaultValue) { + return value ? value : defaultValue +} + +/** + * Resolve resources by mapping corresponding functions to its endpoint + * @param route route + * @reutrn Array + */ +function resolveResources(route) { + const resources = [ + { method: 'get', match: '/', functionName: 'index' }, + { method: 'get', match: '/:id', functionName: 'view' }, + { method: 'post', match: '/', functionName: 'store' }, + { method: 'put', match: '/:id', functionName: 'update' }, + { method: 'patch', match: '/:id', functionName: 'patch' }, + { method: 'del', match: '/:id', functionName: 'delete' } + ] + + return Object.entries(route.controller) + .map(([key, value]) => { + const resource = resources.find(each => each.functionName === key) + + if (resource) { + return { + method: resource.method, + match: appendPrefix(resource.match, route.resources), + action: value + } + } + }).filter(item => item) +} + +/** + * Properly map middlewares regardless if it's array or not + * @param middlewares + * @return Array + */ +function mapMiddleware(middlewares) { + const returnMiddleware = [] + + middlewares.forEach(middleware => { + if (Array.isArray(middleware)) { + returnMiddleware.push(...middleware) + } else { + returnMiddleware.push(middleware) + } + }) + + return returnMiddleware.filter(item => item) +} + +/** + * append additional prefix + * @param route original route + * @param prefix route prefix + * @return string + */ +function appendPrefix(route, prefix: any = false) { + if (prefix) { + route = route.replace(/^\/|\/$/g, '') + prefix = prefix.replace(/^\/|\/$/g, '') + route = `/${prefix}/${route}`.replace('//', '/') + } + + return route.replace(/\/$/, '') +} + +/** + * Group middleware according to it's position + * @param middlewares + * @return Array + */ +function groupMiddleware (middlewares: Array) { + const positions = { + before: [], + after: [] + } + + middlewares.forEach(middleware => { + if (isValidArray(middleware)) { + if (isValidMiddlewarePosition(middleware[0])) { + positions[middleware[0]].push(middleware[1]) + } + } else { + positions.before.push(middleware) + } + }) + + const { before, after } = positions + + return [ + before, + after + ] +} + +/** + * Checks whethere the position supplied is valid + * @param data + * @return boolean + */ +function isValidMiddlewarePosition(data: string) { + return data === 'before' || data === 'after' +} + +/** + * Checks whether the supplied argument is array with values + * @param data + * @return boolean + */ +function isValidArray(data: string | Array) { + return Array.isArray(data) && data.length > 1 +} + +/** + * Flatten your route, takes care of your middleware and everything. + * @param routes + * @return Array + */ +export function transformRoutes(routes) { + routes = routes.length ? sortRoutes( + [].concat( ...routeTranslator(routes) ) + ) : [] + + return routes.map(function (route) { + let action = [ route.action ] + + if (route.middleware.length) { + const [ before, after ] = groupMiddleware(route.middleware) + + action = [ ...before, ...action, ...after] + } + + return { + method: route.method, + endpoint: route.match, + action + } + }) +} + +/** + * takes a restify server as an argument, returns a function + * that takes an array of object. + * @param {Server} server restify server + * @param {boolean} verbose log routing + * @returns routing function + */ +export default function configureRoutes(server : any, verbose = false, logger = console.log) { + return function (routes) { + routes = transformRoutes(routes) + + routes.map(route => { + if (verbose) { + logger(`Routing: [${route.method}] - ${route.endpoint}`) + } + + return server[route.method](route.endpoint, ...route.action) + }) + } +} diff --git a/src/sorts.ts b/src/sorts.ts new file mode 100644 index 0000000..0016089 --- /dev/null +++ b/src/sorts.ts @@ -0,0 +1,107 @@ +/** + * sorts the given routes + * @param {array} routes array of routes + * @returns sorted routes + */ +export function sortRoutes(routes: Array) { + if (!Array.isArray(routes)) { + throw new Error(`argument is expected to be an array of object, [${typeof routes}] type is given.`) + } + + routes = groupBySlashes(routes) + + return groupSortWildcards(routes) +} + +/** + * Group sorted routes based on wildcards + * @param routes + * @return grouped routes + */ +export function groupSortWildcards(routes: Array) { + return Object.entries(routes) + .map(([key, value]) => { + return value.sort(wildCard) + }).reduce((previous, current) => ([ + ...current, + ...previous + ])) +} + +/** + * function for sorting based on slash count + * @param previous previous element + * @param current current element + */ +export function slashCount(previous: any, current: any) { + previous = previous.match.split('/') + current = current.match.split('/') + + const currentSlashes = current.length + const previousSlashes = previous.length + const currentLastPhrase = current[current.length - 1].length + const previousLastPhrase = previous[previous.length -1].length + + if (currentSlashes === previousSlashes) { + return currentLastPhrase - previousLastPhrase + } + + return currentSlashes - previousSlashes +} + +/** + * Group routes based on its slash count + * @param array routes + */ +export function groupBySlashes(array: Array) { + return array.reduce((previous, current) => { + const key = current.match.split('/').length + previous[key] = previous[key] || [] + previous[key].push(current) + + return previous + }, {}) +} + +/** + * function for sorting based on wildcards + * @param previous previous element + * @param current current element + */ +export function wildCard(previous: any, current: any) { + const previousMatches = previous.match.match(/\/(\:[a-zA-Z0-9_-]{1,})/g) + const currentMatches = current.match.match(/\/(\:[a-zA-Z0-9_-]{1,})/g) + const previousUri = previous.match.split('/') + const currentUri = current.match.split('/') + const totalLength = currentUri ? currentUri.length : 0 + + /** + * ensures that those that doesn't have wildcard are pushed to + * top, and full length wildcards are pushed to bottom + */ + if (!previousMatches) { + return -1 + } + + if (!currentMatches) { + return 1 + } + + if (previousMatches.length === (totalLength - 1)) { + return 1 + } + + if(currentMatches.length === (totalLength - 1)) { + return -1 + } + + const previousSum = previousMatches.map(e => { + return previousUri.indexOf(e.replace('/', '')) + }).reduce((p,c) => ((p + c) / previousUri.length)) + + const currentSum = currentMatches.map(e => { + return currentUri.indexOf(e.replace('/', '')) + }).reduce((p,c) => ((p + c) / currentUri.length)) + + return currentSum - previousSum +} diff --git a/test/router.test.ts b/test/router.test.ts new file mode 100644 index 0000000..41eabdc --- /dev/null +++ b/test/router.test.ts @@ -0,0 +1,100 @@ +import { expect } from 'chai' +import router, { transformRoutes, routeTranslator } from '../src/router' +import { sortRoutes } from '../src/sorts' + +describe('restify-router-config', () => { + + const server = { + get: (endpoint, controller) => console.log({ endpoint, controller }), + post: (endpoint, controller) => console.log({ endpoint, controller }), + put: (endpoint, controller) => console.log({ endpoint, controller }), + patch: (endpoint, controller) => console.log({ endpoint, controller }), + del: (endpoint, controller) => console.log({ endpoint, controller }) + } + + const testSubject = [ + { match: '/:foo/:awe' }, + { match: '/:foo/:awe/:awe2' }, + { match: '/:foo/awe/awe2' }, + { match: '/foo' }, + { match: '/foo/:awe/awe2' }, + { match: '/foo/awe/awe2' }, + { match: '/:foo/awe/:awe2' }, + { match: '/foo/:awe/:awe2' }, + { match: '/foo/:awe' }, + { match: '/foo/awe/:awe2' }, + { match: '/:awe/:foo/:bar' }, + { match: '/awe/foo/bar' }, + { match: '/:foo/awe' }, + { match: '/:foo' } + ] + + const testSubjectShouldMatch = [ + { match: '/foo/awe/awe2' }, + { match: '/awe/foo/bar' }, + { match: '/foo/awe/:awe2' }, + { match: '/foo/:awe/awe2' }, + { match: '/foo/:awe/:awe2' }, + { match: '/:foo/awe/awe2' }, + { match: '/:foo/awe/:awe2' }, + { match: '/:awe/:foo/:bar' }, + { match: '/:foo/:awe/:awe2' }, + { match: '/foo/:awe' }, + { match: '/:foo/awe' }, + { match: '/:foo/:awe' }, + { match: '/foo' }, + { match: '/:foo' } + ] + + it('should sort array based on the number of slashes', () => { + const sorted = sortRoutes(testSubject) + + expect(sorted.toString()).to.eq(testSubjectShouldMatch.toString()) + }) + + it('should throw an error if group and resources co-exist', () => { + const throwSomething = function () { + return transformRoutes([ + { + group: '/users', + resources: '/users', + routes: [ + { + match: '/:id', + action: () => {} + } + ] + } + ]) + } + + expect(throwSomething).to.throw() + }) + + it('should throw an error if no controller is speficified', () => { + const throwSomething = () => transformRoutes([ + { + group: '/rooms', + middleware: [ + ['before', function holla() {} ] + ], + routes: [ + { + resources: '/deluxe', + middleware: [ + ['before', function middlewarez() {} ], + ['after', function afterz() {} ] + ] + }, + { + match: '/deluxe/sample/foo/:id', + action: function foo() {}, + middleware: [] + } + ] + } + ]) + + expect(throwSomething).to.throw() + }) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fe74ba2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es3", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "sourceMap": true, + "lib": [ + "esnext" + ] + }, + "exclude": [ + "node_modules" + ] +}