Skip to content

Commit

Permalink
Add querrystring parser (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-tymoshenko authored May 18, 2022
1 parent fcd3e99 commit 349f8e4
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 19 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
})
```
Expand Down
4 changes: 3 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ declare namespace Router {
req: Req<V>,
res: Res<V>,
params: { [k: string]: string | undefined },
store: any
store: any,
searchParams: { [k: string]: string }
) => any;

interface ConstraintStrategy<V extends HTTPVersion, T = string> {
Expand Down Expand Up @@ -110,6 +111,7 @@ declare namespace Router {
handler: Handler<V>;
params: { [k: string]: string | undefined };
store: any;
searchParams: { [k: string]: string };
}

interface Instance<V extends HTTPVersion> {
Expand Down
22 changes: 18 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions lib/url-sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
47 changes: 47 additions & 0 deletions test/custom-querystring-parser.test.js
Original file line number Diff line number Diff line change
@@ -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: '' })
})
8 changes: 4 additions & 4 deletions test/methods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} }
)
})

Expand All @@ -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: {} }
)
})

Expand All @@ -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: {} }
)
})

Expand All @@ -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: {} }
)
})

Expand Down
15 changes: 9 additions & 6 deletions test/querystring.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@ 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')
})

findMyWay.lookup({ method: 'GET', url: '/test?hello=world', headers: {} }, null)
})

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')
})

findMyWay.lookup({ method: 'GET', url: '/test#hello', headers: {} }, null)
})

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')
})

Expand Down
3 changes: 2 additions & 1 deletion test/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
})
})

Expand Down

0 comments on commit 349f8e4

Please sign in to comment.