diff --git a/README.md b/README.md index 582492d..6485d4f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,21 @@ const router = require('find-my-way')({ }) ``` +The default query string parser that find-my-way uses is the Node.js's core querystring module. You can change this default setting by passing the option querystringParser and use a custom one, such as [qs](https://www.npmjs.com/package/qs). + +```js +const qs = require('qs') +const router = require('find-my-way')({ + querystringParser: str => qs.parse(str) +}) + +router.on('GET', '/', (req, res, params, store, searchParams) => { + assert.equal(searchParams, { foo: 'bar', baz: 'faz' }) +}) + +router.lookup({ method: 'GET', url: '/?foo=bar&baz=faz' }, null) +``` + You can assign a `buildPrettyMeta` function to sanitize a route's `store` object to use with the `prettyPrint` functions. This function should accept a single object and return an object. ```js @@ -227,7 +242,7 @@ The signature of the functions and objects must match the one from the example a #### on(method, path, [opts], handler, [store]) Register a new route. ```js -router.on('GET', '/example', (req, res, params) => { +router.on('GET', '/example', (req, res, params, store, searchParams) => { // your code }) ``` diff --git a/index.d.ts b/index.d.ts index 421b5d4..03a83ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -54,7 +54,8 @@ declare namespace Router { req: Req, res: Res, params: { [k: string]: string | undefined }, - store: any + store: any, + searchParams: { [k: string]: string } ) => any; interface ConstraintStrategy { @@ -110,6 +111,7 @@ declare namespace Router { handler: Handler; params: { [k: string]: string | undefined }; store: any; + searchParams: { [k: string]: string }; } interface Instance { diff --git a/index.js b/index.js index 0531b10..63c7062 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,7 @@ const assert = require('assert') const http = require('http') +const querystring = require('querystring') const isRegexSafe = require('safe-regex2') const deepEqual = require('fast-deep-equal') const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode, prettyPrintRoutesArray } = require('./lib/pretty-print') @@ -73,6 +74,13 @@ function Router (opts) { this.buildPrettyMeta = defaultBuildPrettyMeta } + if (opts.querystringParser) { + assert(typeof opts.querystringParser === 'function', 'querystringParser must be a function') + this.querystringParser = opts.querystringParser + } else { + this.querystringParser = (query) => query === '' ? {} : querystring.parse(query) + } + this.caseSensitive = opts.caseSensitive === undefined ? true : opts.caseSensitive this.ignoreTrailingSlash = opts.ignoreTrailingSlash || false this.maxParamLength = opts.maxParamLength || 100 @@ -318,8 +326,8 @@ Router.prototype.lookup = function lookup (req, res, ctx) { var handle = this.find(req.method, req.url, this.constrainer.deriveConstraints(req, ctx)) if (handle === null) return this._defaultRoute(req, res, ctx) return ctx === undefined - ? handle.handler(req, res, handle.params, handle.store) - : handle.handler.call(ctx, req, res, handle.params, handle.store) + ? handle.handler(req, res, handle.params, handle.store, handle.searchParams) + : handle.handler.call(ctx, req, res, handle.params, handle.store, handle.searchParams) } Router.prototype.find = function find (method, path, derivedConstraints) { @@ -330,8 +338,13 @@ Router.prototype.find = function find (method, path, derivedConstraints) { path = path.replace(FULL_PATH_REGEXP, '/') } + let sanitizedUrl + let querystring + try { - path = safeDecodeURI(path) + sanitizedUrl = safeDecodeURI(path) + path = sanitizedUrl.path + querystring = sanitizedUrl.querystring } catch (error) { return this._onBadUrl(path) } @@ -361,8 +374,9 @@ Router.prototype.find = function find (method, path, derivedConstraints) { if (handle !== null) { return { handler: handle.handler, + store: handle.store, params: handle._createParamsObject(params), - store: handle.store + searchParams: this.querystringParser(querystring) } } } diff --git a/lib/url-sanitizer.js b/lib/url-sanitizer.js index 368ba72..9af1f6e 100644 --- a/lib/url-sanitizer.js +++ b/lib/url-sanitizer.js @@ -36,6 +36,7 @@ function decodeComponentChar (highCharCode, lowCharCode) { function safeDecodeURI (path) { let shouldDecode = false + let querystring = '' for (let i = 1; i < path.length; i++) { const charCode = path.charCodeAt(i) @@ -59,14 +60,16 @@ function safeDecodeURI (path) { // string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`. // Thus, we need to split on `;` as well as `?` and `#`. } else if (charCode === 63 || charCode === 59 || charCode === 35) { + querystring = path.slice(i + 1) path = path.slice(0, i) break } } - return shouldDecode ? decodeURI(path) : path + const decodedPath = shouldDecode ? decodeURI(path) : path + return { path: decodedPath, querystring } } -function safeDecodeURIComponent (uriComponent, startIndex = 0) { +function safeDecodeURIComponent (uriComponent, startIndex) { let decoded = '' let lastIndex = startIndex diff --git a/test/custom-querystring-parser.test.js b/test/custom-querystring-parser.test.js new file mode 100644 index 0000000..4944233 --- /dev/null +++ b/test/custom-querystring-parser.test.js @@ -0,0 +1,47 @@ +'use strict' + +const t = require('tap') +const test = t.test +const querystring = require('querystring') +const FindMyWay = require('../') + +test('Custom querystring parser', t => { + t.plan(2) + + const findMyWay = FindMyWay({ + querystringParser: function (str) { + t.equal(str, 'foo=bar&baz=faz') + return querystring.parse(str) + } + }) + findMyWay.on('GET', '/', () => {}) + + t.same(findMyWay.find('GET', '/?foo=bar&baz=faz').searchParams, { foo: 'bar', baz: 'faz' }) +}) + +test('Custom querystring parser should be called also if there is nothing to parse', t => { + t.plan(2) + + const findMyWay = FindMyWay({ + querystringParser: function (str) { + t.equal(str, '') + return querystring.parse(str) + } + }) + findMyWay.on('GET', '/', () => {}) + + t.same(findMyWay.find('GET', '/').searchParams, {}) +}) + +test('Querystring without value', t => { + t.plan(2) + + const findMyWay = FindMyWay({ + querystringParser: function (str) { + t.equal(str, 'foo') + return querystring.parse(str) + } + }) + findMyWay.on('GET', '/', () => {}) + t.same(findMyWay.find('GET', '/?foo').searchParams, { foo: '' }) +}) diff --git a/test/methods.test.js b/test/methods.test.js index 4f1e0e1..c32a5a2 100644 --- a/test/methods.test.js +++ b/test/methods.test.js @@ -435,7 +435,7 @@ test('find should return the route', t => { t.same( findMyWay.find('GET', '/test'), - { handler: fn, params: {}, store: null } + { handler: fn, params: {}, store: null, searchParams: {} } ) }) @@ -448,7 +448,7 @@ test('find should return the route with params', t => { t.same( findMyWay.find('GET', '/test/hello'), - { handler: fn, params: { id: 'hello' }, store: null } + { handler: fn, params: { id: 'hello' }, store: null, searchParams: {} } ) }) @@ -471,7 +471,7 @@ test('should decode the uri - parametric', t => { t.same( findMyWay.find('GET', '/test/he%2Fllo'), - { handler: fn, params: { id: 'he/llo' }, store: null } + { handler: fn, params: { id: 'he/llo' }, store: null, searchParams: {} } ) }) @@ -484,7 +484,7 @@ test('should decode the uri - wildcard', t => { t.same( findMyWay.find('GET', '/test/he%2Fllo'), - { handler: fn, params: { '*': 'he/llo' }, store: null } + { handler: fn, params: { '*': 'he/llo' }, store: null, searchParams: {} } ) }) diff --git a/test/querystring.test.js b/test/querystring.test.js index a48c8f4..cf015c6 100644 --- a/test/querystring.test.js +++ b/test/querystring.test.js @@ -5,10 +5,11 @@ const test = t.test const FindMyWay = require('../') test('should sanitize the url - query', t => { - t.plan(1) + t.plan(2) const findMyWay = FindMyWay() - findMyWay.on('GET', '/test', (req, res, params) => { + findMyWay.on('GET', '/test', (req, res, params, store, query) => { + t.same(query, { hello: 'world' }) t.ok('inside the handler') }) @@ -16,10 +17,11 @@ test('should sanitize the url - query', t => { }) test('should sanitize the url - hash', t => { - t.plan(1) + t.plan(2) const findMyWay = FindMyWay() - findMyWay.on('GET', '/test', (req, res, params) => { + findMyWay.on('GET', '/test', (req, res, params, store, query) => { + t.same(query, { hello: '' }) t.ok('inside the handler') }) @@ -27,10 +29,11 @@ test('should sanitize the url - hash', t => { }) test('handles path and query separated by ;', t => { - t.plan(1) + t.plan(2) const findMyWay = FindMyWay() - findMyWay.on('GET', '/test', (req, res, params) => { + findMyWay.on('GET', '/test', (req, res, params, store, query) => { + t.same(query, { jsessionid: '123456' }) t.ok('inside the handler') }) diff --git a/test/store.test.js b/test/store.test.js index c1f6c04..bba91e3 100644 --- a/test/store.test.js +++ b/test/store.test.js @@ -25,7 +25,8 @@ test('find a store object', t => { t.same(findMyWay.find('GET', '/test'), { handler: fn, params: {}, - store: { hello: 'world' } + store: { hello: 'world' }, + searchParams: {} }) })