-
Notifications
You must be signed in to change notification settings - Fork 352
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(utils): Support arrays & nested objects in query params (#148)
- Loading branch information
1 parent
955da6b
commit 7e846b0
Showing
6 changed files
with
182 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import URLParse from 'url-parse'; | ||
import qs from 'qs'; | ||
|
||
const ARRAY_FORMAT = Symbol(); | ||
const INDICES_REGEX = /\[\d+\]$/; | ||
const BRACKETS_REGEX = /\[\]$/; | ||
|
||
function parseQuery(query, options) { | ||
return qs.parse(query, { | ||
plainObjects: true, | ||
ignoreQueryPrefix: true, | ||
...options | ||
}); | ||
} | ||
|
||
function stringifyQuery(obj, options = {}) { | ||
return qs.stringify(obj, { addQueryPrefix: true, ...options }); | ||
} | ||
|
||
/** | ||
* Given a query string, determine the array format used. Returns `undefined` | ||
* if one cannot be determined. | ||
* | ||
* @param {String} query | ||
* @returns {String | undefined} | ||
*/ | ||
function arrayFormat(query) { | ||
const keys = (query || '') | ||
.replace('?', '') | ||
.split('&') | ||
.map(str => decodeURIComponent(str.split('=')[0])); | ||
|
||
for (const key of keys) { | ||
if (INDICES_REGEX.test(key)) { | ||
// a[0]=b&a[1]=c | ||
return 'indices'; | ||
} else if (BRACKETS_REGEX.test(key)) { | ||
// a[]=b&a[]=c | ||
return 'brackets'; | ||
} | ||
} | ||
|
||
// Look to see if any key has a duplicate | ||
const hasDuplicate = keys.some((key, index) => keys.indexOf(key) !== index); | ||
|
||
if (hasDuplicate) { | ||
// 'a=b&a=c' | ||
return 'repeat'; | ||
} | ||
} | ||
|
||
/** | ||
* An extended url-parse class that uses `qs` instead of the default | ||
* `querystringify` to support array and nested object query param strings. | ||
*/ | ||
export default class URL extends URLParse { | ||
constructor(url, parse) { | ||
// Construct the url with an un-parsed querystring | ||
super(url); | ||
|
||
if (parse) { | ||
// If we want the querystring to be parsed, use this.set('query', query) | ||
// as it will always parse the string. If there is no initial querystring | ||
// pass an object which will act as the parsed query. | ||
this.set('query', this.query || {}); | ||
} | ||
} | ||
|
||
/** | ||
* Override set for `query` so we can pass it our custom parser. | ||
* https://github.com/unshiftio/url-parse/blob/1.4.4/index.js#L314-L316 | ||
* | ||
* @override | ||
*/ | ||
set(part, value, fn) { | ||
if (part === 'query') { | ||
if (value && typeof value === 'string') { | ||
// Save the array format used so when we stringify it, | ||
// we can use the correct format. | ||
this[ARRAY_FORMAT] = arrayFormat(value) || this[ARRAY_FORMAT]; | ||
} | ||
|
||
return super.set(part, value, parseQuery); | ||
} | ||
|
||
return super.set(part, value, fn); | ||
} | ||
|
||
/** | ||
* Override toString so we can pass it our custom query stringify method. | ||
* https://github.com/unshiftio/url-parse/blob/1.4.4/index.js#L414 | ||
* | ||
* @override | ||
*/ | ||
toString() { | ||
return super.toString(obj => | ||
stringifyQuery(obj, { arrayFormat: this[ARRAY_FORMAT] }) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import URL from '../../../src/utils/url'; | ||
|
||
const encode = encodeURIComponent; | ||
const decode = decodeURIComponent; | ||
|
||
describe('Unit | Utils | URL', function() { | ||
it('should exist', function() { | ||
expect(URL).to.be.a('function'); | ||
}); | ||
|
||
it('should work', function() { | ||
expect(new URL('http://netflix.com').href).to.equal('http://netflix.com'); | ||
}); | ||
|
||
it('should should not parse the query string by default', function() { | ||
expect(new URL('http://netflix.com?foo=bar').query).to.equal('?foo=bar'); | ||
}); | ||
|
||
it('should correctly parse query params', function() { | ||
[ | ||
['', {}], | ||
['foo=bar', { foo: 'bar' }], | ||
['a[]=1&a[]=2', { a: ['1', '2'] }], | ||
['a[1]=1&a[0]=2', { a: ['2', '1'] }], | ||
['a=1&a=2', { a: ['1', '2'] }], | ||
['foo[bar][baz]=1', { foo: { bar: { baz: '1' } } }] | ||
].forEach(([query, obj]) => { | ||
expect(new URL(`http://foo.bar?${query}`, true).query).to.deep.equal(obj); | ||
}); | ||
}); | ||
|
||
it('should correctly stringify query params', function() { | ||
[ | ||
// Query string will be undefined but we decode it in the assertion | ||
[{}, decode(undefined)], | ||
[{ foo: 'bar' }, 'foo=bar'], | ||
[{ a: ['1', '2'] }, 'a[0]=1&a[1]=2'], | ||
[{ foo: { bar: { baz: '1' } } }, 'foo[bar][baz]=1'] | ||
].forEach(([obj, query]) => { | ||
const url = new URL('http://foo.bar', true); | ||
|
||
url.set('query', obj); | ||
expect(decode(url.href.split('?')[1])).to.equal(query); | ||
expect(decode(url.toString().split('?')[1])).to.equal(query); | ||
}); | ||
}); | ||
|
||
it('should correctly detect original array formats', function() { | ||
[ | ||
'a[0]=1&a[1]=2', | ||
`${encode('a[0]')}=1&${encode('a[1]')}=2`, | ||
'a[]=1&a[]=2', | ||
`${encode('a[]')}=1&${encode('a[]')}=2`, | ||
'a=1&a=2' | ||
].forEach(query => { | ||
const url = new URL(`http://foo.bar?${query}`, true); | ||
|
||
expect(decode(url.href.split('?')[1])).to.equal(decode(query)); | ||
expect(decode(url.toString().split('?')[1])).to.equal(decode(query)); | ||
}); | ||
}); | ||
|
||
it('should correctly handle changes in array formats', function() { | ||
const url = new URL(`http://foo.bar`, true); | ||
|
||
['a[0]=1&a[1]=2', 'a[]=1&a[]=2', 'a=1&a=2'].forEach(query => { | ||
url.set('query', query); | ||
|
||
expect(decode(url.href.split('?')[1])).to.equal(query); | ||
expect(decode(url.toString().split('?')[1])).to.equal(query); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11133,6 +11133,11 @@ [email protected], qs@^6.4.0, qs@~6.5.1, qs@~6.5.2: | |
version "6.5.2" | ||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" | ||
|
||
qs@^6.6.0: | ||
version "6.6.0" | ||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2" | ||
integrity sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA== | ||
|
||
qs@~6.4.0: | ||
version "6.4.0" | ||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" | ||
|