diff --git a/.editorconfig b/.editorconfig index 86c8f59f..1c6314a3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,9 +7,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[package.json] +[*.yml] indent_style = space indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 176a458f..6313b56c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=auto +* text=auto eol=lf diff --git a/.github/funding.yml b/.github/funding.yml new file mode 100644 index 00000000..07c8db1c --- /dev/null +++ b/.github/funding.yml @@ -0,0 +1,4 @@ +github: sindresorhus +open_collective: sindresorhus +tidelift: npm/query-string +custom: https://sindresorhus.com/donate diff --git a/.github/security.md b/.github/security.md new file mode 100644 index 00000000..5358dc50 --- /dev/null +++ b/.github/security.md @@ -0,0 +1,3 @@ +# Security Policy + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..b85fc2a9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 14 + - 12 + - 10 + - 8 + - 6 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index 3c3629e6..239ecff1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +yarn.lock diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 804f8af7..00000000 --- a/.jshintrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "node": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "immed": true, - "newcap": true, - "noarg": true, - "undef": true, - "unused": "vars", - "strict": true -} diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dedfc07f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -sudo: false -language: node_js -node_js: - - 'iojs' - - '0.12' - - '0.10' diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 00000000..b1524608 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,75 @@ +'use strict'; +const Benchmark = require('benchmark'); +const queryString = require('.'); + +const {stringify, stringifyUrl} = queryString; +const suite = new Benchmark.Suite(); + +// Fixtures +const TEST_OBJECT = { + genre: 'Epic fantasy', + author: '', + page: 2, + published: true, + symbols: 'πµ', + chapters: [1, 2, 3], + none: null +}; +const TEST_HOST = 'https://foo.bar/'; +const TEST_STRING = stringify(TEST_OBJECT); +const TEST_BRACKETS_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket'}); +const TEST_INDEX_STRING = stringify(TEST_OBJECT, {arrayFormat: 'index'}); +const TEST_COMMA_STRING = stringify(TEST_OBJECT, {arrayFormat: 'comma'}); +const TEST_BRACKET_SEPARATOR_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket-separator'}); +const TEST_URL = stringifyUrl({url: TEST_HOST, query: TEST_OBJECT}); + +// Creates a test case and adds it to the suite +const defineTestCase = (methodName, input, options) => { + const fn = queryString[methodName]; + const label = options ? ` (${stringify(options)})` : ''; + + suite.add(methodName + label, () => fn(input, options || {})); +}; + +// Define all test cases + +// Parse +defineTestCase('parse', TEST_STRING); +defineTestCase('parse', TEST_STRING, {parseNumbers: true}); +defineTestCase('parse', TEST_STRING, {parseBooleans: true}); +defineTestCase('parse', TEST_STRING, {sort: false}); +defineTestCase('parse', TEST_STRING, {decode: false}); +defineTestCase('parse', TEST_BRACKETS_STRING, {arrayFormat: 'bracket'}); +defineTestCase('parse', TEST_INDEX_STRING, {arrayFormat: 'index'}); +defineTestCase('parse', TEST_COMMA_STRING, {arrayFormat: 'comma'}); +defineTestCase('parse', TEST_BRACKET_SEPARATOR_STRING, {arrayFormat: 'bracket-separator'}); + +// Stringify +defineTestCase('stringify', TEST_OBJECT); +defineTestCase('stringify', TEST_OBJECT, {strict: false}); +defineTestCase('stringify', TEST_OBJECT, {encode: false}); +defineTestCase('stringify', TEST_OBJECT, {skipNull: true}); +defineTestCase('stringify', TEST_OBJECT, {skipEmptyString: true}); +defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket'}); +defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'index'}); +defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'comma'}); +defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket-separator'}); + +// Extract +defineTestCase('extract', TEST_URL); + +// ParseUrl +defineTestCase('parseUrl', TEST_URL); + +// StringifyUrl +defineTestCase('stringifyUrl', {url: TEST_HOST, query: TEST_OBJECT}); + +// Log/display the results +suite.on('cycle', event => { + const {name, hz} = event.target; + const opsPerSec = Math.round(hz).toLocaleString(); + + console.log(name.padEnd(46, '_') + opsPerSec.padStart(3, '_') + ' ops/s'); +}); + +suite.run(); diff --git a/bower.json b/bower.json deleted file mode 100644 index 1d694f8f..00000000 --- a/bower.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "query-string", - "description": "Parse and stringify URL query strings", - "main": "query-string.js", - "keywords": [ - "querystring", - "query", - "string", - "qs", - "param", - "parameter", - "url", - "uri", - "parse", - "stringify", - "encode", - "decode" - ], - "ignore": [ - ".*", - "test.js", - "component.json", - "package.json" - ] -} diff --git a/component.json b/component.json deleted file mode 100644 index 68ea7f40..00000000 --- a/component.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "query-string", - "version": "0.4.1", - "description": "Parse and stringify URL query strings", - "repository": "sindresorhus/query-string", - "keywords": [ - "querystring", - "query", - "string", - "qs", - "param", - "parameter", - "url", - "uri", - "parse", - "stringify", - "encode", - "decode" - ], - "main": "query-string.js", - "scripts": ["query-string.js"], - "license": "MIT" -} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..847336d4 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,545 @@ +export interface ParseOptions { + /** + Decode the keys and values. URI components are decoded with [`decode-uri-component`](https://github.com/SamVerschueren/decode-uri-component). + + @default true + */ + readonly decode?: boolean; + + /** + @default 'none' + + - `bracket`: Parse arrays with bracket representation: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo[]=1&foo[]=2&foo[]=3', {arrayFormat: 'bracket'}); + //=> {foo: ['1', '2', '3']} + ``` + + - `index`: Parse arrays with index representation: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3', {arrayFormat: 'index'}); + //=> {foo: ['1', '2', '3']} + ``` + + - `comma`: Parse arrays with elements separated by comma: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo=1,2,3', {arrayFormat: 'comma'}); + //=> {foo: ['1', '2', '3']} + ``` + + - `separator`: Parse arrays with elements separated by a custom character: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator: '|'}); + //=> {foo: ['1', '2', '3']} + ``` + + - `bracket-separator`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: []} + + queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: ['']} + + queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: ['1']} + + queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: ['1', '2', '3']} + + queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: ['1', '', 3, '', '', '6']} + + queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']} + ``` + + - `none`: Parse arrays with elements using duplicate keys: + + ``` + import queryString = require('query-string'); + + queryString.parse('foo=1&foo=2&foo=3'); + //=> {foo: ['1', '2', '3']} + ``` + */ + readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none'; + + /** + The character used to separate array elements when using `{arrayFormat: 'separator'}`. + + @default , + */ + readonly arrayFormatSeparator?: string; + + /** + Supports both `Function` as a custom sorting function or `false` to disable sorting. + + If omitted, keys are sorted using `Array#sort`, which means, converting them to strings and comparing strings in Unicode code point order. + + @default true + + @example + ``` + import queryString = require('query-string'); + + const order = ['c', 'a', 'b']; + + queryString.parse('?a=one&b=two&c=three', { + sort: (itemLeft, itemRight) => order.indexOf(itemLeft) - order.indexOf(itemRight) + }); + //=> {c: 'three', a: 'one', b: 'two'} + ``` + + @example + ``` + import queryString = require('query-string'); + + queryString.parse('?a=one&c=three&b=two', {sort: false}); + //=> {a: 'one', c: 'three', b: 'two'} + ``` + */ + readonly sort?: ((itemLeft: string, itemRight: string) => number) | false; + + /** + Parse the value as a number type instead of string type if it's a number. + + @default false + + @example + ``` + import queryString = require('query-string'); + + queryString.parse('foo=1', {parseNumbers: true}); + //=> {foo: 1} + ``` + */ + readonly parseNumbers?: boolean; + + /** + Parse the value as a boolean type instead of string type if it's a boolean. + + @default false + + @example + ``` + import queryString = require('query-string'); + + queryString.parse('foo=true', {parseBooleans: true}); + //=> {foo: true} + ``` + */ + readonly parseBooleans?: boolean; + + /** + Parse the fragment identifier from the URL and add it to result object. + + @default false + + @example + ``` + import queryString = require('query-string'); + + queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true}); + //=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'} + ``` + */ + readonly parseFragmentIdentifier?: boolean; +} + +export interface ParsedQuery { + [key: string]: T | T[] | null; +} + +/** +Parse a query string into an object. Leading `?` or `#` are ignored, so you can pass `location.search` or `location.hash` directly. + +The returned object is created with [`Object.create(null)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) and thus does not have a `prototype`. + +@param query - The query string to parse. +*/ +export function parse(query: string, options: {parseBooleans: true, parseNumbers: true} & ParseOptions): ParsedQuery; +export function parse(query: string, options: {parseBooleans: true} & ParseOptions): ParsedQuery; +export function parse(query: string, options: {parseNumbers: true} & ParseOptions): ParsedQuery; +export function parse(query: string, options?: ParseOptions): ParsedQuery; + +export interface ParsedUrl { + readonly url: string; + readonly query: ParsedQuery; + + /** + The fragment identifier of the URL. + + Present when the `parseFragmentIdentifier` option is `true`. + */ + readonly fragmentIdentifier?: string; +} + +/** +Extract the URL and the query string as an object. + +If the `parseFragmentIdentifier` option is `true`, the object will also contain a `fragmentIdentifier` property. + +@param url - The URL to parse. + +@example +``` +import queryString = require('query-string'); + +queryString.parseUrl('https://foo.bar?foo=bar'); +//=> {url: 'https://foo.bar', query: {foo: 'bar'}} + +queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true}); +//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'} +``` +*/ +export function parseUrl(url: string, options?: ParseOptions): ParsedUrl; + +export interface StringifyOptions { + /** + Strictly encode URI components with [`strict-uri-encode`](https://github.com/kevva/strict-uri-encode). It uses [`encodeURIComponent`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) if set to `false`. You probably [don't care](https://github.com/sindresorhus/query-string/issues/42) about this option. + + @default true + */ + readonly strict?: boolean; + + /** + [URL encode](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) the keys and values. + + @default true + */ + readonly encode?: boolean; + + /** + @default 'none' + + - `bracket`: Serialize arrays using bracket representation: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket'}); + //=> 'foo[]=1&foo[]=2&foo[]=3' + ``` + + - `index`: Serialize arrays using index representation: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'index'}); + //=> 'foo[0]=1&foo[1]=2&foo[2]=3' + ``` + + - `comma`: Serialize arrays by separating elements with comma: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'}); + //=> 'foo=1,2,3' + + queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'}); + //=> 'foo=1,,' + // Note that typing information for null values is lost + // and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`. + ``` + + - `separator`: Serialize arrays by separating elements with character: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'}); + //=> 'foo=1|2|3' + ``` + + - `bracket-separator`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]' + + queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]=' + + queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]=1' + + queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]=1|2|3' + + queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]=1||3|||6' + + queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true}); + //=> 'foo[]=1||3|6' + + queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); + //=> 'foo[]=1|2|3&bar=fluffy&baz[]=4' + ``` + + - `none`: Serialize arrays by using duplicate keys: + + ``` + import queryString = require('query-string'); + + queryString.stringify({foo: [1, 2, 3]}); + //=> 'foo=1&foo=2&foo=3' + ``` + */ + readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none'; + + /** + The character used to separate array elements when using `{arrayFormat: 'separator'}`. + + @default , + */ + readonly arrayFormatSeparator?: string; + + /** + Supports both `Function` as a custom sorting function or `false` to disable sorting. + + If omitted, keys are sorted using `Array#sort`, which means, converting them to strings and comparing strings in Unicode code point order. + + @default true + + @example + ``` + import queryString = require('query-string'); + + const order = ['c', 'a', 'b']; + + queryString.stringify({a: 1, b: 2, c: 3}, { + sort: (itemLeft, itemRight) => order.indexOf(itemLeft) - order.indexOf(itemRight) + }); + //=> 'c=3&a=1&b=2' + ``` + + @example + ``` + import queryString = require('query-string'); + + queryString.stringify({b: 1, c: 2, a: 3}, {sort: false}); + //=> 'b=1&c=2&a=3' + ``` + */ + readonly sort?: ((itemLeft: string, itemRight: string) => number) | false; + + /** + Skip keys with `null` as the value. + + Note that keys with `undefined` as the value are always skipped. + + @default false + + @example + ``` + import queryString = require('query-string'); + + queryString.stringify({a: 1, b: undefined, c: null, d: 4}, { + skipNull: true + }); + //=> 'a=1&d=4' + + queryString.stringify({a: undefined, b: null}, { + skipNull: true + }); + //=> '' + ``` + */ + readonly skipNull?: boolean; + + /** + Skip keys with an empty string as the value. + + @default false + + @example + ``` + import queryString = require('query-string'); + + queryString.stringify({a: 1, b: '', c: '', d: 4}, { + skipEmptyString: true + }); + //=> 'a=1&d=4' + ``` + + @example + ``` + import queryString = require('query-string'); + + queryString.stringify({a: '', b: ''}, { + skipEmptyString: true + }); + //=> '' + ``` + */ + readonly skipEmptyString?: boolean; +} + +export type Stringifiable = string | boolean | number | null | undefined; + +export type StringifiableRecord = Record< + string, + Stringifiable | readonly Stringifiable[] +>; + +/** +Stringify an object into a query string and sort the keys. +*/ +export function stringify( + // TODO: Use the below instead when the following TS issues are fixed: + // - https://github.com/microsoft/TypeScript/issues/15300 + // - https://github.com/microsoft/TypeScript/issues/42021 + // Context: https://github.com/sindresorhus/query-string/issues/298 + // object: StringifiableRecord, + object: Record, + options?: StringifyOptions +): string; + +/** +Extract a query string from a URL that can be passed into `.parse()`. + +Note: This behaviour can be changed with the `skipNull` option. +*/ +export function extract(url: string): string; + +export interface UrlObject { + readonly url: string; + + /** + Overrides queries in the `url` property. + */ + readonly query?: StringifiableRecord; + + /** + Overrides the fragment identifier in the `url` property. + */ + readonly fragmentIdentifier?: string; +} + +/** +Stringify an object into a URL with a query string and sorting the keys. The inverse of [`.parseUrl()`](https://github.com/sindresorhus/query-string#parseurlstring-options) + +Query items in the `query` property overrides queries in the `url` property. + +The `fragmentIdentifier` property overrides the fragment identifier in the `url` property. + +@example +``` +queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}}); +//=> 'https://foo.bar?foo=bar' + +queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}); +//=> 'https://foo.bar?foo=bar' + +queryString.stringifyUrl({ + url: 'https://foo.bar', + query: { + top: 'foo' + }, + fragmentIdentifier: 'bar' +}); +//=> 'https://foo.bar?top=foo#bar' +``` +*/ +export function stringifyUrl( + object: UrlObject, + options?: StringifyOptions +): string; + +/** +Pick query parameters from a URL. + +@param url - The URL containing the query parameters to pick. +@param keys - The names of the query parameters to keep. All other query parameters will be removed from the URL. +@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +@returns The URL with the picked query parameters. + +@example +``` +queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?foo=1#hello' + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?bar=2#hello' +``` +*/ +export function pick( + url: string, + keys: readonly string[], + options?: ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | boolean | number) => boolean, + options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | boolean) => boolean, + options?: {parseBooleans: true} & ParseOptions & StringifyOptions +): string +export function pick( + url: string, + filter: (key: string, value: string | number) => boolean, + options?: {parseNumbers: true} & ParseOptions & StringifyOptions +): string + +/** +Exclude query parameters from a URL. Like `.pick()` but reversed. + +@param url - The URL containing the query parameters to exclude. +@param keys - The names of the query parameters to remove. All other query parameters will remain in the URL. +@param filter - A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +@returns The URL without the excluded the query parameters. + +@example +``` +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?bar=2#hello' + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?foo=1#hello' +``` +*/ +export function exclude( + url: string, + keys: readonly string[], + options?: ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | boolean | number) => boolean, + options?: {parseBooleans: true, parseNumbers: true} & ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | boolean) => boolean, + options?: {parseBooleans: true} & ParseOptions & StringifyOptions +): string +export function exclude( + url: string, + filter: (key: string, value: string | number) => boolean, + options?: {parseNumbers: true} & ParseOptions & StringifyOptions +): string diff --git a/index.js b/index.js new file mode 100644 index 00000000..cc576373 --- /dev/null +++ b/index.js @@ -0,0 +1,447 @@ +'use strict'; +const strictUriEncode = require('strict-uri-encode'); +const decodeComponent = require('decode-uri-component'); +const splitOnFirst = require('split-on-first'); +const filterObject = require('filter-obj'); + +const isNullOrUndefined = value => value === null || value === undefined; + +const encodeFragmentIdentifier = Symbol('encodeFragmentIdentifier'); + +function encoderForArrayFormat(options) { + switch (options.arrayFormat) { + case 'index': + return key => (result, value) => { + const index = result.length; + + if ( + value === undefined || + (options.skipNull && value === null) || + (options.skipEmptyString && value === '') + ) { + return result; + } + + if (value === null) { + return [...result, [encode(key, options), '[', index, ']'].join('')]; + } + + return [ + ...result, + [encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('') + ]; + }; + + case 'bracket': + return key => (result, value) => { + if ( + value === undefined || + (options.skipNull && value === null) || + (options.skipEmptyString && value === '') + ) { + return result; + } + + if (value === null) { + return [...result, [encode(key, options), '[]'].join('')]; + } + + return [...result, [encode(key, options), '[]=', encode(value, options)].join('')]; + }; + + case 'comma': + case 'separator': + case 'bracket-separator': { + const keyValueSep = options.arrayFormat === 'bracket-separator' ? + '[]=' : + '='; + + return key => (result, value) => { + if ( + value === undefined || + (options.skipNull && value === null) || + (options.skipEmptyString && value === '') + ) { + return result; + } + + // Translate null to an empty string so that it doesn't serialize as 'null' + value = value === null ? '' : value; + + if (result.length === 0) { + return [[encode(key, options), keyValueSep, encode(value, options)].join('')]; + } + + return [[result, encode(value, options)].join(options.arrayFormatSeparator)]; + }; + } + + default: + return key => (result, value) => { + if ( + value === undefined || + (options.skipNull && value === null) || + (options.skipEmptyString && value === '') + ) { + return result; + } + + if (value === null) { + return [...result, encode(key, options)]; + } + + return [...result, [encode(key, options), '=', encode(value, options)].join('')]; + }; + } +} + +function parserForArrayFormat(options) { + let result; + + switch (options.arrayFormat) { + case 'index': + return (key, value, accumulator) => { + result = /\[(\d*)\]$/.exec(key); + + key = key.replace(/\[\d*\]$/, ''); + + if (!result) { + accumulator[key] = value; + return; + } + + if (accumulator[key] === undefined) { + accumulator[key] = {}; + } + + accumulator[key][result[1]] = value; + }; + + case 'bracket': + return (key, value, accumulator) => { + result = /(\[\])$/.exec(key); + key = key.replace(/\[\]$/, ''); + + if (!result) { + accumulator[key] = value; + return; + } + + if (accumulator[key] === undefined) { + accumulator[key] = [value]; + return; + } + + accumulator[key] = [].concat(accumulator[key], value); + }; + + case 'comma': + case 'separator': + return (key, value, accumulator) => { + const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator); + const isEncodedArray = (typeof value === 'string' && !isArray && decode(value, options).includes(options.arrayFormatSeparator)); + value = isEncodedArray ? decode(value, options) : value; + const newValue = isArray || isEncodedArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options); + accumulator[key] = newValue; + }; + + case 'bracket-separator': + return (key, value, accumulator) => { + const isArray = /(\[\])$/.test(key); + key = key.replace(/\[\]$/, ''); + + if (!isArray) { + accumulator[key] = value ? decode(value, options) : value; + return; + } + + const arrayValue = value === null ? + [] : + value.split(options.arrayFormatSeparator).map(item => decode(item, options)); + + if (accumulator[key] === undefined) { + accumulator[key] = arrayValue; + return; + } + + accumulator[key] = [].concat(accumulator[key], arrayValue); + }; + + default: + return (key, value, accumulator) => { + if (accumulator[key] === undefined) { + accumulator[key] = value; + return; + } + + accumulator[key] = [].concat(accumulator[key], value); + }; + } +} + +function validateArrayFormatSeparator(value) { + if (typeof value !== 'string' || value.length !== 1) { + throw new TypeError('arrayFormatSeparator must be single character string'); + } +} + +function encode(value, options) { + if (options.encode) { + return options.strict ? strictUriEncode(value) : encodeURIComponent(value); + } + + return value; +} + +function decode(value, options) { + if (options.decode) { + return decodeComponent(value); + } + + return value; +} + +function keysSorter(input) { + if (Array.isArray(input)) { + return input.sort(); + } + + if (typeof input === 'object') { + return keysSorter(Object.keys(input)) + .sort((a, b) => Number(a) - Number(b)) + .map(key => input[key]); + } + + return input; +} + +function removeHash(input) { + const hashStart = input.indexOf('#'); + if (hashStart !== -1) { + input = input.slice(0, hashStart); + } + + return input; +} + +function getHash(url) { + let hash = ''; + const hashStart = url.indexOf('#'); + if (hashStart !== -1) { + hash = url.slice(hashStart); + } + + return hash; +} + +function extract(input) { + input = removeHash(input); + const queryStart = input.indexOf('?'); + if (queryStart === -1) { + return ''; + } + + return input.slice(queryStart + 1); +} + +function parseValue(value, options) { + if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) { + value = Number(value); + } else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { + value = value.toLowerCase() === 'true'; + } + + return value; +} + +function parse(query, options) { + options = Object.assign({ + decode: true, + sort: true, + arrayFormat: 'none', + arrayFormatSeparator: ',', + parseNumbers: false, + parseBooleans: false + }, options); + + validateArrayFormatSeparator(options.arrayFormatSeparator); + + const formatter = parserForArrayFormat(options); + + // Create an object with no prototype + const ret = Object.create(null); + + if (typeof query !== 'string') { + return ret; + } + + query = query.trim().replace(/^[?#&]/, ''); + + if (!query) { + return ret; + } + + for (const param of query.split('&')) { + if (param === '') { + continue; + } + + let [key, value] = splitOnFirst(options.decode ? param.replace(/\+/g, ' ') : param, '='); + + // Missing `=` should be `null`: + // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters + value = value === undefined ? null : ['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options); + formatter(decode(key, options), value, ret); + } + + for (const key of Object.keys(ret)) { + const value = ret[key]; + if (typeof value === 'object' && value !== null) { + for (const k of Object.keys(value)) { + value[k] = parseValue(value[k], options); + } + } else { + ret[key] = parseValue(value, options); + } + } + + if (options.sort === false) { + return ret; + } + + return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => { + const value = ret[key]; + if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) { + // Sort object keys, not values + result[key] = keysSorter(value); + } else { + result[key] = value; + } + + return result; + }, Object.create(null)); +} + +exports.extract = extract; +exports.parse = parse; + +exports.stringify = (object, options) => { + if (!object) { + return ''; + } + + options = Object.assign({ + encode: true, + strict: true, + arrayFormat: 'none', + arrayFormatSeparator: ',' + }, options); + + validateArrayFormatSeparator(options.arrayFormatSeparator); + + const shouldFilter = key => ( + (options.skipNull && isNullOrUndefined(object[key])) || + (options.skipEmptyString && object[key] === '') + ); + + const formatter = encoderForArrayFormat(options); + + const objectCopy = {}; + + for (const key of Object.keys(object)) { + if (!shouldFilter(key)) { + objectCopy[key] = object[key]; + } + } + + const keys = Object.keys(objectCopy); + + if (options.sort !== false) { + keys.sort(options.sort); + } + + return keys.map(key => { + const value = object[key]; + + if (value === undefined) { + return ''; + } + + if (value === null) { + return encode(key, options); + } + + if (Array.isArray(value)) { + if (value.length === 0 && options.arrayFormat === 'bracket-separator') { + return encode(key, options) + '[]'; + } + + return value + .reduce(formatter(key), []) + .join('&'); + } + + return encode(key, options) + '=' + encode(value, options); + }).filter(x => x.length > 0).join('&'); +}; + +exports.parseUrl = (url, options) => { + options = Object.assign({ + decode: true + }, options); + + const [url_, hash] = splitOnFirst(url, '#'); + + return Object.assign( + { + url: url_.split('?')[0] || '', + query: parse(extract(url), options) + }, + options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {} + ); +}; + +exports.stringifyUrl = (object, options) => { + options = Object.assign({ + encode: true, + strict: true, + [encodeFragmentIdentifier]: true + }, options); + + const url = removeHash(object.url).split('?')[0] || ''; + const queryFromUrl = exports.extract(object.url); + const parsedQueryFromUrl = exports.parse(queryFromUrl, {sort: false}); + + const query = Object.assign(parsedQueryFromUrl, object.query); + let queryString = exports.stringify(query, options); + if (queryString) { + queryString = `?${queryString}`; + } + + let hash = getHash(object.url); + if (object.fragmentIdentifier) { + hash = `#${options[encodeFragmentIdentifier] ? encode(object.fragmentIdentifier, options) : object.fragmentIdentifier}`; + } + + return `${url}${queryString}${hash}`; +}; + +exports.pick = (input, filter, options) => { + options = Object.assign({ + parseFragmentIdentifier: true, + [encodeFragmentIdentifier]: false + }, options); + + const {url, query, fragmentIdentifier} = exports.parseUrl(input, options); + return exports.stringifyUrl({ + url, + query: filterObject(query, filter), + fragmentIdentifier + }, options); +}; + +exports.exclude = (input, filter, options) => { + const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value); + + return exports.pick(input, exclusionFilter, options); +}; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 00000000..20325841 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,132 @@ +import {expectType} from 'tsd'; +import * as queryString from '.'; + +// Stringify +expectType( + queryString.stringify({ + str: 'bar', + strArray: ['baz'], + num: 123, + numArray: [456], + bool: true, + boolArray: [false] + }) +); + +expectType(queryString.stringify({foo: 'bar'}, {strict: false})); +expectType(queryString.stringify({foo: 'bar'}, {encode: false})); +expectType( + queryString.stringify({foo: 'bar'}, {arrayFormat: 'bracket'}) +); +expectType(queryString.stringify({foo: 'bar'}, {arrayFormat: 'index'})); +expectType(queryString.stringify({foo: 'bar'}, {arrayFormat: 'none'})); +expectType(queryString.stringify({foo: 'bar'}, {arrayFormat: 'comma'})); +expectType(queryString.stringify({foo: 'bar'}, {sort: false})); +expectType(queryString.stringify({foo: 'bar'}, {skipNull: true})); +expectType(queryString.stringify({foo: 'bar'}, {skipEmptyString: true})); +const order = ['c', 'a', 'b']; +expectType( + queryString.stringify( + {foo: 'bar'}, + { + sort: (itemLeft, itemRight) => + order.indexOf(itemLeft) - order.indexOf(itemRight) + } + ) +); + +// Ensure it accepts an `interface`. +interface Query { + foo: string; +} + +const query: Query = { + foo: 'bar' +}; + +queryString.stringify(query); + +// Parse +expectType(queryString.parse('?foo=bar')); + +expectType( + queryString.parse('?foo=bar', {decode: false}) +); +expectType( + queryString.parse('?foo=bar', {arrayFormat: 'bracket'}) +); +expectType( + queryString.parse('?foo=bar', {arrayFormat: 'index'}) +); +expectType( + queryString.parse('?foo=bar', {arrayFormat: 'none'}) +); +expectType( + queryString.parse('?foo=bar', {arrayFormat: 'comma'}) +); +expectType>( + queryString.parse('?foo=1', {parseNumbers: true}) +); +expectType>( + queryString.parse('?foo=true', {parseBooleans: true}) +); +expectType>( + queryString.parse('?foo=true', {parseBooleans: true, parseNumbers: true}) +); + +// Parse URL +expectType(queryString.parseUrl('?foo=bar')); + +expectType( + queryString.parseUrl('?foo=bar', {decode: false}) +); +expectType( + queryString.parseUrl('?foo=bar', {arrayFormat: 'bracket'}) +); +expectType( + queryString.parseUrl('?foo=bar', {arrayFormat: 'index'}) +); +expectType( + queryString.parseUrl('?foo=bar', {arrayFormat: 'none'}) +); +expectType( + queryString.parseUrl('?foo=bar', {arrayFormat: 'comma'}) +); +expectType( + queryString.parseUrl('?foo=1', {parseNumbers: true}) +); +expectType( + queryString.parseUrl('?foo=true', {parseBooleans: true}) +); +expectType( + queryString.parseUrl('?foo=true#bar', {parseFragmentIdentifier: true}) +); + +// Extract +expectType(queryString.extract('http://foo.bar/?abc=def&hij=klm')); + +expectType( + queryString.stringifyUrl({ + url: 'https://sindresorhus.com', + query: { + fooMixedArray: [ + 'a', + 1, + true, + null, + undefined + ], + fooNumber: 1, + fooBoolean: true, + fooNull: null, + fooUndefined: undefined, + fooString: 'hi' + }, + }) +); + +// Pick +expectType(queryString.pick('http://foo.bar/?abc=def&hij=klm', ['abc'])) + +// Exclude +expectType(queryString.exclude('http://foo.bar/?abc=def&hij=klm', ['abc'])) diff --git a/license b/license index 654d0bfe..e464bf78 100644 --- a/license +++ b/license @@ -1,21 +1,9 @@ -The MIT License (MIT) +MIT License -Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Sindre Sorhus (http://sindresorhus.com) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json index 3cb2813a..c1a4a75e 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,54 @@ { - "name": "query-string", - "version": "1.0.1", - "description": "Parse and stringify URL query strings", - "keywords": [ - "browser", - "querystring", - "query", - "string", - "qs", - "param", - "parameter", - "url", - "uri", - "parse", - "stringify", - "encode", - "decode" - ], - "license": "MIT", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "http://sindresorhus.com" - }, - "files": [ - "query-string.js" - ], - "main": "query-string.js", - "repository": "sindresorhus/query-string", - "scripts": { - "test": "mocha" - }, - "devDependencies": { - "mocha": "*" - }, - "engines": { - "node": ">=0.10.0" - } + "name": "query-string", + "version": "7.0.1", + "description": "Parse and stringify URL query strings", + "license": "MIT", + "repository": "sindresorhus/query-string", + "funding": "https://github.com/sponsors/sindresorhus", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "https://sindresorhus.com" + }, + "engines": { + "node": ">=6" + }, + "scripts": { + "benchmark": "node benchmark.js", + "test": "xo && ava && tsd" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "browser", + "querystring", + "query", + "string", + "qs", + "param", + "parameter", + "url", + "parse", + "stringify", + "encode", + "decode", + "searchparams", + "filter" + ], + "dependencies": { + "decode-uri-component": "^0.2.0", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "devDependencies": { + "ava": "^1.4.1", + "benchmark": "^2.1.4", + "deep-equal": "^1.0.1", + "fast-check": "^1.5.0", + "tsd": "^0.7.3", + "xo": "^0.24.0" + } } diff --git a/query-string.js b/query-string.js deleted file mode 100644 index 5fcb02e4..00000000 --- a/query-string.js +++ /dev/null @@ -1,66 +0,0 @@ -/*! - query-string - Parse and stringify URL query strings - https://github.com/sindresorhus/query-string - by Sindre Sorhus - MIT License -*/ -(function () { - 'use strict'; - var queryString = {}; - - queryString.parse = function (str) { - if (typeof str !== 'string') { - return {}; - } - - str = str.trim().replace(/^(\?|#)/, ''); - - if (!str) { - return {}; - } - - return str.trim().split('&').reduce(function (ret, param) { - var parts = param.replace(/\+/g, ' ').split('='); - var key = parts[0]; - var val = parts[1]; - - key = decodeURIComponent(key); - // missing `=` should be `null`: - // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters - val = val === undefined ? null : decodeURIComponent(val); - - if (!ret.hasOwnProperty(key)) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - - return ret; - }, {}); - }; - - queryString.stringify = function (obj) { - return obj ? Object.keys(obj).map(function (key) { - var val = obj[key]; - - if (Array.isArray(val)) { - return val.map(function (val2) { - return encodeURIComponent(key) + '=' + encodeURIComponent(val2); - }).join('&'); - } - - return encodeURIComponent(key) + '=' + encodeURIComponent(val); - }).join('&') : ''; - }; - - if (typeof define === 'function' && define.amd) { - define(function() { return queryString; }); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = queryString; - } else { - self.queryString = queryString; - } -})(); diff --git a/readme.md b/readme.md index a7a4e386..3af49af9 100644 --- a/readme.md +++ b/readme.md @@ -1,78 +1,633 @@ -# query-string [![Build Status](https://travis-ci.org/sindresorhus/query-string.svg?branch=master)](https://travis-ci.org/sindresorhus/query-string) +# query-string -> Parse and stringify URL [query strings](http://en.wikipedia.org/wiki/Query_string) +> Parse and stringify URL [query strings](https://en.wikipedia.org/wiki/Query_string) +
+ +--- + + + +--- + +
## Install -```sh -$ npm install --save query-string ``` - -```sh -$ bower install --save query-string +$ npm install query-string ``` -```sh -$ component install sindresorhus/query-string -``` +**Not `npm install querystring`!!!!!** +This module targets Node.js 6 or later and the latest version of Chrome, Firefox, and Safari. ## Usage ```js +const queryString = require('query-string'); + console.log(location.search); -// ?foo=bar +//=> '?foo=bar' -var parsed = queryString.parse(location.search); +const parsed = queryString.parse(location.search); console.log(parsed); -// {foo: 'bar'} +//=> {foo: 'bar'} console.log(location.hash); -// #token=bada55cafe +//=> '#token=bada55cafe' -var parsedHash = queryString.parse(location.hash); +const parsedHash = queryString.parse(location.hash); console.log(parsedHash); -// {token: 'bada55cafe'} +//=> {token: 'bada55cafe'} parsed.foo = 'unicorn'; parsed.ilike = 'pizza'; -location.search = queryString.stringify(parsed); +const stringified = queryString.stringify(parsed); +//=> 'foo=unicorn&ilike=pizza' +location.search = stringified; +// note that `location.search` automatically prepends a question mark console.log(location.search); -// ?foo=unicorn&ilike=pizza +//=> '?foo=unicorn&ilike=pizza' ``` - ## API -### queryString.parse(*string*) +### .parse(string, options?) Parse a query string into an object. Leading `?` or `#` are ignored, so you can pass `location.search` or `location.hash` directly. -### queryString.stringify(*object*) +The returned object is created with [`Object.create(null)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create) and thus does not have a `prototype`. + +#### options + +Type: `object` + +##### decode + +Type: `boolean`\ +Default: `true` + +Decode the keys and values. URL components are decoded with [`decode-uri-component`](https://github.com/SamVerschueren/decode-uri-component). + +##### arrayFormat + +Type: `string`\ +Default: `'none'` + +- `'bracket'`: Parse arrays with bracket representation: + +```js +const queryString = require('query-string'); + +queryString.parse('foo[]=1&foo[]=2&foo[]=3', {arrayFormat: 'bracket'}); +//=> {foo: ['1', '2', '3']} +``` + +- `'index'`: Parse arrays with index representation: + +```js +const queryString = require('query-string'); + +queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3', {arrayFormat: 'index'}); +//=> {foo: ['1', '2', '3']} +``` + +- `'comma'`: Parse arrays with elements separated by comma: + +```js +const queryString = require('query-string'); + +queryString.parse('foo=1,2,3', {arrayFormat: 'comma'}); +//=> {foo: ['1', '2', '3']} +``` + +- `'separator'`: Parse arrays with elements separated by a custom character: + +```js +const queryString = require('query-string'); + +queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator: '|'}); +//=> {foo: ['1', '2', '3']} +``` + +- `'bracket-separator'`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character: + +```js +const queryString = require('query-string'); + +queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: []} + +queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: ['']} + +queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: ['1']} + +queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: ['1', '2', '3']} + +queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: ['1', '', 3, '', '', '6']} + +queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']} +``` + +- `'none'`: Parse arrays with elements using duplicate keys: + +```js +const queryString = require('query-string'); + +queryString.parse('foo=1&foo=2&foo=3'); +//=> {foo: ['1', '2', '3']} +``` + +##### arrayFormatSeparator + +Type: `string`\ +Default: `','` + +The character used to separate array elements when using `{arrayFormat: 'separator'}`. + +##### sort + +Type: `Function | boolean`\ +Default: `true` + +Supports both `Function` as a custom sorting function or `false` to disable sorting. + +##### parseNumbers + +Type: `boolean`\ +Default: `false` + +```js +const queryString = require('query-string'); + +queryString.parse('foo=1', {parseNumbers: true}); +//=> {foo: 1} +``` + +Parse the value as a number type instead of string type if it's a number. + +##### parseBooleans + +Type: `boolean`\ +Default: `false` + +```js +const queryString = require('query-string'); + +queryString.parse('foo=true', {parseBooleans: true}); +//=> {foo: true} +``` + +Parse the value as a boolean type instead of string type if it's a boolean. + +### .stringify(object, options?) + +Stringify an object into a query string and sorting the keys. + +#### options + +Type: `object` + +##### strict + +Type: `boolean`\ +Default: `true` + +Strictly encode URI components with [strict-uri-encode](https://github.com/kevva/strict-uri-encode). It uses [encodeURIComponent](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) if set to false. You probably [don't care](https://github.com/sindresorhus/query-string/issues/42) about this option. + +##### encode + +Type: `boolean`\ +Default: `true` + +[URL encode](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) the keys and values. + +##### arrayFormat + +Type: `string`\ +Default: `'none'` + +- `'bracket'`: Serialize arrays using bracket representation: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket'}); +//=> 'foo[]=1&foo[]=2&foo[]=3' +``` + +- `'index'`: Serialize arrays using index representation: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'index'}); +//=> 'foo[0]=1&foo[1]=2&foo[2]=3' +``` + +- `'comma'`: Serialize arrays by separating elements with comma: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'}); +//=> 'foo=1,2,3' + +queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'}); +//=> 'foo=1,,' +// Note that typing information for null values is lost +// and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`. +``` + +- `'separator'`: Serialize arrays by separating elements with a custom character: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'}); +//=> 'foo=1|2|3' +``` + +- `'bracket-separator'`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]' + +queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]=' + +queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]=1' + +queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]=1|2|3' + +queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]=1||3|||6' + +queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true}); +//=> 'foo[]=1||3|6' + +queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'}); +//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4' +``` + +- `'none'`: Serialize arrays by using duplicate keys: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: [1, 2, 3]}); +//=> 'foo=1&foo=2&foo=3' +``` + +##### arrayFormatSeparator + +Type: `string`\ +Default: `','` + +The character used to separate array elements when using `{arrayFormat: 'separator'}`. + +##### sort + +Type: `Function | boolean` + +Supports both `Function` as a custom sorting function or `false` to disable sorting. -Stringify an object into a query string. +```js +const queryString = require('query-string'); + +const order = ['c', 'a', 'b']; + +queryString.stringify({a: 1, b: 2, c: 3}, { + sort: (a, b) => order.indexOf(a) - order.indexOf(b) +}); +//=> 'c=3&a=1&b=2' +``` + +```js +const queryString = require('query-string'); + +queryString.stringify({b: 1, c: 2, a: 3}, {sort: false}); +//=> 'b=1&c=2&a=3' +``` + +If omitted, keys are sorted using `Array#sort()`, which means, converting them to strings and comparing strings in Unicode code point order. + +##### skipNull + +Skip keys with `null` as the value. + +Note that keys with `undefined` as the value are always skipped. + +Type: `boolean`\ +Default: `false` + +```js +const queryString = require('query-string'); + +queryString.stringify({a: 1, b: undefined, c: null, d: 4}, { + skipNull: true +}); +//=> 'a=1&d=4' +``` + +```js +const queryString = require('query-string'); + +queryString.stringify({a: undefined, b: null}, { + skipNull: true +}); +//=> '' +``` + +##### skipEmptyString + +Skip keys with an empty string as the value. + +Type: `boolean`\ +Default: `false` + +```js +const queryString = require('query-string'); + +queryString.stringify({a: 1, b: '', c: '', d: 4}, { + skipEmptyString: true +}); +//=> 'a=1&d=4' +``` + +```js +const queryString = require('query-string'); + +queryString.stringify({a: '', b: ''}, { + skipEmptyString: true +}); +//=> '' +``` + +### .extract(string) + +Extract a query string from a URL that can be passed into `.parse()`. + +Note: This behaviour can be changed with the `skipNull` option. + +### .parseUrl(string, options?) + +Extract the URL and the query string as an object. + +Returns an object with a `url` and `query` property. + +If the `parseFragmentIdentifier` option is `true`, the object will also contain a `fragmentIdentifier` property. + +```js +const queryString = require('query-string'); + +queryString.parseUrl('https://foo.bar?foo=bar'); +//=> {url: 'https://foo.bar', query: {foo: 'bar'}} + +queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true}); +//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'} +``` + +#### options + +Type: `object` + +The options are the same as for `.parse()`. + +Extra options are as below. + +##### parseFragmentIdentifier + +Parse the fragment identifier from the URL. + +Type: `boolean`\ +Default: `false` + +```js +const queryString = require('query-string'); + +queryString.parseUrl('https://foo.bar?foo=bar#xyz', {parseFragmentIdentifier: true}); +//=> {url: 'https://foo.bar', query: {foo: 'bar'}, fragmentIdentifier: 'xyz'} +``` + +### .stringifyUrl(object, options?) + +Stringify an object into a URL with a query string and sorting the keys. The inverse of [`.parseUrl()`](https://github.com/sindresorhus/query-string#parseurlstring-options) + +The `options` are the same as for `.stringify()`. +Returns a string with the URL and a query string. + +Query items in the `query` property overrides queries in the `url` property. + +The `fragmentIdentifier` property overrides the fragment identifier in the `url` property. + +```js +queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}}); +//=> 'https://foo.bar?foo=bar' + +queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}); +//=> 'https://foo.bar?foo=bar' + +queryString.stringifyUrl({ + url: 'https://foo.bar', + query: { + top: 'foo' + }, + fragmentIdentifier: 'bar' +}); +//=> 'https://foo.bar?top=foo#bar' +``` + +#### object + +Type: `object` + +##### url + +Type: `string` + +The URL to stringify. + +##### query + +Type: `object` + +Query items to add to the URL. + +### .pick(url, keys, options?) +### .pick(url, filter, options?) + +Pick query parameters from a URL. + +Returns a string with the new URL. + +```js +const queryString = require('query-string'); + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?foo=1#hello' + +queryString.pick('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?bar=2#hello' +``` + +### .exclude(url, keys, options?) +### .exclude(url, filter, options?) + +Exclude query parameters from a URL. + +Returns a string with the new URL. + +```js +const queryString = require('query-string'); + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', ['foo']); +//=> 'https://foo.bar?bar=2#hello' + +queryString.exclude('https://foo.bar?foo=1&bar=2#hello', (name, value) => value === 2, {parseNumbers: true}); +//=> 'https://foo.bar?foo=1#hello' +``` + +#### url + +Type: `string` + +The URL containing the query parameters to filter. + +#### keys + +Type: `string[]` + +The names of the query parameters to filter based on the function used. + +#### filter + +Type: `(key, value) => boolean` + +A filter predicate that will be provided the name of each query parameter and its value. The `parseNumbers` and `parseBooleans` options also affect `value`. + +#### options + +Type: `object` + +[Parse options](#options) and [stringify options](#options-1). ## Nesting -This module intentionally doesn't support nesting as it's not specced and varies between implementations, which causes a lot of [edge cases](https://github.com/visionmedia/node-querystring/issues). +This module intentionally doesn't support nesting as it's not spec'd and varies between implementations, which causes a lot of [edge cases](https://github.com/visionmedia/node-querystring/issues). You're much better off just converting the object to a JSON string: ```js +const queryString = require('query-string'); + queryString.stringify({ - foo: 'bar', - nested: JSON.stringify({ - unicorn: 'cake' - }) + foo: 'bar', + nested: JSON.stringify({ + unicorn: 'cake' + }) }); -// foo=bar&nested=%7B%22unicorn%22%3A%22cake%22%7D +//=> 'foo=bar&nested=%7B%22unicorn%22%3A%22cake%22%7D' ``` +However, there is support for multiple instances of the same key: + +```js +const queryString = require('query-string'); + +queryString.parse('likes=cake&name=bob&likes=icecream'); +//=> {likes: ['cake', 'icecream'], name: 'bob'} + +queryString.stringify({color: ['taupe', 'chartreuse'], id: '515'}); +//=> 'color=taupe&color=chartreuse&id=515' +``` + +## Falsy values + +Sometimes you want to unset a key, or maybe just make it present without assigning a value to it. Here is how falsy values are stringified: + +```js +const queryString = require('query-string'); + +queryString.stringify({foo: false}); +//=> 'foo=false' + +queryString.stringify({foo: null}); +//=> 'foo' + +queryString.stringify({foo: undefined}); +//=> '' +``` + +## FAQ + +### Why is it parsing `+` as a space? + +See [this answer](https://github.com/sindresorhus/query-string/issues/305). + +## query-string for enterprise -## License +Available as part of the Tidelift Subscription. -MIT © [Sindre Sorhus](http://sindresorhus.com) +The maintainers of query-string and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-query-string?utm_source=npm-query-string&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/test.js b/test.js deleted file mode 100644 index 8730973f..00000000 --- a/test.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; -var assert = require('assert'); -var qs = require('./query-string'); - -describe('.parse()', function () { - it('should handle query strings starting with a `?`', function () { - assert.deepEqual(qs.parse('?foo=bar'), {foo: 'bar'}); - }); - - it('should handle query strings starting with a `#`', function () { - assert.deepEqual(qs.parse('#foo=bar'), {foo: 'bar'}); - }); - - it('should parse a qseter', function () { - assert.deepEqual(qs.parse('foo=bar'), {foo: 'bar'}); - }); - - it('should parse multiple qseters', function () { - assert.deepEqual(qs.parse('foo=bar&key=val'), {foo: 'bar', key: 'val'}); - }); - - it('should parse qseters without a value', function () { - assert.deepEqual(qs.parse('foo'), {foo: null}); - assert.deepEqual(qs.parse('foo&key'), {foo: null, key: null}); - assert.deepEqual(qs.parse('foo=bar&key'), {foo: 'bar', key: null}); - }); - - it('should return empty object if no qss can be found', function () { - assert.deepEqual(qs.parse('?'), {}); - assert.deepEqual(qs.parse('#'), {}); - assert.deepEqual(qs.parse(' '), {}); - }); - - it('should handle `+` correctly', function () { - assert.deepEqual(qs.parse('foo+faz=bar+baz++'), {'foo faz': 'bar baz '}); - }); - - it('should handle multiple of the same key', function () { - assert.deepEqual(qs.parse('foo=bar&foo=baz'), {foo: ['bar', 'baz']}); - }); -}); - -describe('.stringify()', function () { - it('should stringify', function () { - assert.strictEqual(qs.stringify({foo: 'bar'}), 'foo=bar'); - assert.strictEqual(qs.stringify({foo: 'bar', bar: 'baz'}), 'foo=bar&bar=baz'); - }); - - it('should handle different types', function () { - assert.strictEqual(qs.stringify(), ''); - assert.strictEqual(qs.stringify(0), ''); - }); - - it('should URI encode', function () { - assert.strictEqual(qs.stringify({'foo bar': 'baz faz'}), 'foo%20bar=baz%20faz'); - }); - - it('should handle array value', function () { - assert.strictEqual(qs.stringify({abc: 'abc', foo: ['bar', 'baz']}), 'abc=abc&foo=bar&foo=baz'); - }); -}); diff --git a/test/exclude.js b/test/exclude.js new file mode 100644 index 00000000..646db886 --- /dev/null +++ b/test/exclude.js @@ -0,0 +1,21 @@ +import test from 'ava'; +import queryString from '..'; + +test('excludes elements in a URL with a filter array', t => { + t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', ['c']), 'http://example.com/?a=1&b=2#a'); +}); + +test('excludes elements in a URL with a filter predicate', t => { + t.is(queryString.exclude('http://example.com/?a=1&b=2&c=3#a', (name, value) => { + t.is(typeof name, 'string'); + t.is(typeof value, 'number'); + + return name === 'a'; + }, { + parseNumbers: true + }), 'http://example.com/?b=2&c=3#a'); +}); + +test('excludes elements in a URL without encoding fragment identifiers', t => { + t.is(queryString.exclude('https://example.com?a=b#/home', ['a']), 'https://example.com#/home'); +}); diff --git a/test/extract.js b/test/extract.js new file mode 100644 index 00000000..b7cde183 --- /dev/null +++ b/test/extract.js @@ -0,0 +1,26 @@ +import test from 'ava'; +import queryString from '..'; + +test('extracts query string from url', t => { + t.is(queryString.extract('https://foo.bar/?abc=def&hij=klm'), 'abc=def&hij=klm'); + t.is(queryString.extract('https://foo.bar/?'), ''); + t.is(queryString.extract('https://foo.bar/?regex=ab?c'), 'regex=ab?c'); + t.is(queryString.extract('https://foo.bar#top?foo=bar'), ''); + t.is(queryString.extract('https://foo.bar?foo=bar#top'), 'foo=bar'); +}); + +test('handles strings not containing query string', t => { + t.is(queryString.extract('https://foo.bar'), ''); + t.is(queryString.extract('https://foo.bar#top'), ''); + t.is(queryString.extract(''), ''); +}); + +test('throws for invalid values', t => { + t.throws(() => { + queryString.extract(null); + }, TypeError); + + t.throws(() => { + queryString.extract(undefined); + }, TypeError); +}); diff --git a/test/parse-url.js b/test/parse-url.js new file mode 100644 index 00000000..720333c4 --- /dev/null +++ b/test/parse-url.js @@ -0,0 +1,36 @@ +import test from 'ava'; +import queryString from '..'; + +test('handles strings with query string', t => { + t.deepEqual(queryString.parseUrl('https://foo.bar#top?foo=bar'), {url: 'https://foo.bar', query: {}}); + t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar&foo=baz#top'), {url: 'https://foo.bar', query: {foo: ['bar', 'baz']}}); + t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar&foo=baz'), {url: 'https://foo.bar', query: {foo: ['bar', 'baz']}}); +}); + +test('handles strings not containing query string', t => { + t.deepEqual(queryString.parseUrl('https://foo.bar/'), {url: 'https://foo.bar/', query: {}}); + t.deepEqual(queryString.parseUrl('https://foo.bar/#top'), {url: 'https://foo.bar/', query: {}}); + t.deepEqual(queryString.parseUrl(''), {url: '', query: {}}); +}); + +test('handles strings with query string that contain =', t => { + t.deepEqual(queryString.parseUrl('https://foo.bar?foo=baz=bar&foo=baz#top'), {url: 'https://foo.bar', query: {foo: ['baz=bar', 'baz']}}); + t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar=&foo=baz='), {url: 'https://foo.bar', query: {foo: ['bar=', 'baz=']}}); +}); + +test('handles strings with fragment identifier', t => { + t.deepEqual(queryString.parseUrl('https://foo.bar?top=foo#bar', {parseFragmentIdentifier: true}), {url: 'https://foo.bar', query: {top: 'foo'}, fragmentIdentifier: 'bar'}); + t.deepEqual(queryString.parseUrl('https://foo.bar?foo=bar&foo=baz#top', {parseFragmentIdentifier: true}), {url: 'https://foo.bar', query: {foo: ['bar', 'baz']}, fragmentIdentifier: 'top'}); + t.deepEqual(queryString.parseUrl('https://foo.bar/#top', {parseFragmentIdentifier: true}), {url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'top'}); + t.deepEqual(queryString.parseUrl('https://foo.bar/#st%C3%A5le', {parseFragmentIdentifier: true}), {url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'ståle'}); +}); + +test('throws for invalid values', t => { + t.throws(() => { + queryString.parseUrl(null); + }, TypeError); + + t.throws(() => { + queryString.parseUrl(undefined); + }, TypeError); +}); diff --git a/test/parse.js b/test/parse.js new file mode 100644 index 00000000..731a8488 --- /dev/null +++ b/test/parse.js @@ -0,0 +1,392 @@ +import test from 'ava'; +import queryString from '..'; + +test('query strings starting with a `?`', t => { + t.deepEqual(queryString.parse('?foo=bar'), {foo: 'bar'}); +}); + +test('query strings starting with a `#`', t => { + t.deepEqual(queryString.parse('#foo=bar'), {foo: 'bar'}); +}); + +test('query strings starting with a `&`', t => { + t.deepEqual(queryString.parse('&foo=bar&foo=baz'), {foo: ['bar', 'baz']}); +}); + +test('query strings ending with a `&`', t => { + t.deepEqual(queryString.parse('foo=bar&'), {foo: 'bar'}); + t.deepEqual(queryString.parse('foo=bar&&&'), {foo: 'bar'}); +}); + +test('parse a query string', t => { + t.deepEqual(queryString.parse('foo=bar'), {foo: 'bar'}); +}); + +test('parse multiple query string', t => { + t.deepEqual(queryString.parse('foo=bar&key=val'), { + foo: 'bar', + key: 'val' + }); +}); + +test('parse multiple query string retain order when not sorted', t => { + const expectedKeys = ['b', 'a', 'c']; + const parsed = queryString.parse('b=foo&a=bar&c=yay', {sort: false}); + Object.keys(parsed).forEach((key, index) => { + t.is(key, expectedKeys[index]); + }); +}); + +test('parse multiple query string sorted keys', t => { + const fixture = ['a', 'b', 'c']; + const parsed = queryString.parse('a=foo&c=bar&b=yay'); + Object.keys(parsed).forEach((key, index) => { + t.is(key, fixture[index]); + }); +}); + +test('should sort parsed keys in given order', t => { + const fixture = ['c', 'a', 'b']; + const sort = (key1, key2) => fixture.indexOf(key1) - fixture.indexOf(key2); + + const parsed = queryString.parse('a=foo&b=bar&c=yay', {sort}); + Object.keys(parsed).forEach((key, index) => { + t.is(key, fixture[index]); + }); +}); + +test('parse query string without a value', t => { + t.deepEqual(queryString.parse('foo'), {foo: null}); + t.deepEqual(queryString.parse('foo&key'), { + foo: null, + key: null + }); + t.deepEqual(queryString.parse('foo=bar&key'), { + foo: 'bar', + key: null + }); + t.deepEqual(queryString.parse('a&a'), {a: [null, null]}); + t.deepEqual(queryString.parse('a=&a'), {a: ['', null]}); +}); + +test('return empty object if no qss can be found', t => { + t.deepEqual(queryString.parse('?'), {}); + t.deepEqual(queryString.parse('&'), {}); + t.deepEqual(queryString.parse('#'), {}); + t.deepEqual(queryString.parse(' '), {}); +}); + +test('handle `+` correctly', t => { + t.deepEqual(queryString.parse('foo+faz=bar+baz++'), {'foo faz': 'bar baz '}); +}); + +test('parses numbers with exponential notation as string', t => { + t.deepEqual(queryString.parse('192e11=bar'), {'192e11': 'bar'}); + t.deepEqual(queryString.parse('bar=192e11'), {bar: '192e11'}); +}); + +test('handle `+` correctly when not decoding', t => { + t.deepEqual(queryString.parse('foo+faz=bar+baz++', {decode: false}), {'foo+faz': 'bar+baz++'}); +}); + +test('handle multiple of the same key', t => { + t.deepEqual(queryString.parse('foo=bar&foo=baz'), {foo: ['bar', 'baz']}); +}); + +test('handle multiple values and preserve appearence order', t => { + t.deepEqual(queryString.parse('a=value&a='), {a: ['value', '']}); + t.deepEqual(queryString.parse('a=&a=value'), {a: ['', 'value']}); +}); + +test('handle multiple values and preserve appearance order with brackets', t => { + t.deepEqual(queryString.parse('a[]=value&a[]=', {arrayFormat: 'bracket'}), {a: ['value', '']}); + t.deepEqual(queryString.parse('a[]=&a[]=value', {arrayFormat: 'bracket'}), {a: ['', 'value']}); +}); + +test('handle multiple values and preserve appearance order with indexes', t => { + t.deepEqual(queryString.parse('a[0]=value&a[1]=', {arrayFormat: 'index'}), {a: ['value', '']}); + t.deepEqual(queryString.parse('a[1]=&a[0]=value', {arrayFormat: 'index'}), {a: ['value', '']}); +}); + +test('query strings params including embedded `=`', t => { + t.deepEqual(queryString.parse('?param=https%3A%2F%2Fsomeurl%3Fid%3D2837'), {param: 'https://someurl?id=2837'}); +}); + +test('object properties', t => { + t.falsy(queryString.parse().prototype); + t.deepEqual(queryString.parse('hasOwnProperty=foo'), {hasOwnProperty: 'foo'}); +}); + +test('query strings having indexed arrays', t => { + t.deepEqual(queryString.parse('foo[0]=bar&foo[1]=baz'), {'foo[0]': 'bar', 'foo[1]': 'baz'}); +}); + +test('query strings having brackets arrays', t => { + t.deepEqual(queryString.parse('foo[]=bar&foo[]=baz'), {'foo[]': ['bar', 'baz']}); +}); + +test('query strings having indexed arrays keeping index order', t => { + t.deepEqual(queryString.parse('foo[1]=bar&foo[0]=baz'), {'foo[1]': 'bar', 'foo[0]': 'baz'}); +}); + +test('query string having a single bracketed value and format option as `bracket`', t => { + t.deepEqual(queryString.parse('foo[]=bar', {arrayFormat: 'bracket'}), {foo: ['bar']}); +}); + +test('query string not having a bracketed value and format option as `bracket`', t => { + t.deepEqual(queryString.parse('foo=bar', {arrayFormat: 'bracket'}), {foo: 'bar'}); +}); + +test('query string having a bracketed value and a single value and format option as `bracket`', t => { + t.deepEqual(queryString.parse('foo=bar&baz[]=bar', {arrayFormat: 'bracket'}), {foo: 'bar', baz: ['bar']}); +}); + +test('query strings having brackets arrays and format option as `bracket`', t => { + t.deepEqual(queryString.parse('foo[]=bar&foo[]=baz', { + arrayFormat: 'bracket' + }), {foo: ['bar', 'baz']}); +}); + +test('query strings having comma separated arrays and format option as `comma`', t => { + t.deepEqual(queryString.parse('foo=bar,baz', { + arrayFormat: 'comma' + }), {foo: ['bar', 'baz']}); +}); + +test('query strings having pipe separated arrays and format option as `separator`', t => { + t.deepEqual(queryString.parse('foo=bar|baz', { + arrayFormat: 'separator', + arrayFormatSeparator: '|' + }), {foo: ['bar', 'baz']}); +}); + +test('query strings having brackets arrays with null and format option as `bracket`', t => { + t.deepEqual(queryString.parse('bar[]&foo[]=a&foo[]&foo[]=', { + arrayFormat: 'bracket' + }), { + foo: ['a', null, ''], + bar: [null] + }); +}); + +test('query strings having comma separated arrays with null and format option as `comma`', t => { + t.deepEqual(queryString.parse('bar&foo=a,', { + arrayFormat: 'comma' + }), { + foo: ['a', ''], + bar: null + }); +}); + +test('query strings having indexed arrays and format option as `index`', t => { + t.deepEqual(queryString.parse('foo[0]=bar&foo[1]=baz', { + arrayFormat: 'index' + }), {foo: ['bar', 'baz']}); +}); + +test('query strings having brackets+separator arrays and format option as `bracket-separator` with 1 value', t => { + t.deepEqual(queryString.parse('foo[]=bar', { + arrayFormat: 'bracket-separator' + }), {foo: ['bar']}); +}); + +test('query strings having brackets+separator arrays and format option as `bracket-separator` with multiple values', t => { + t.deepEqual(queryString.parse('foo[]=bar,baz,,,biz', { + arrayFormat: 'bracket-separator' + }), {foo: ['bar', 'baz', '', '', 'biz']}); +}); + +test('query strings with multiple brackets+separator arrays and format option as `bracket-separator` using same key name', t => { + t.deepEqual(queryString.parse('foo[]=bar,baz&foo[]=biz,boz', { + arrayFormat: 'bracket-separator' + }), {foo: ['bar', 'baz', 'biz', 'boz']}); +}); + +test('query strings having an empty brackets+separator array and format option as `bracket-separator`', t => { + t.deepEqual(queryString.parse('foo[]', { + arrayFormat: 'bracket-separator' + }), {foo: []}); +}); + +test('query strings having a brackets+separator array and format option as `bracket-separator` with a single empty string', t => { + t.deepEqual(queryString.parse('foo[]=', { + arrayFormat: 'bracket-separator' + }), {foo: ['']}); +}); + +test('query strings having = within parameters (i.e. GraphQL IDs)', t => { + t.deepEqual(queryString.parse('foo=bar=&foo=ba=z='), {foo: ['bar=', 'ba=z=']}); +}); + +test('query strings having ordered index arrays and format option as `index`', t => { + t.deepEqual(queryString.parse('foo[1]=bar&foo[0]=baz&foo[3]=one&foo[2]=two', { + arrayFormat: 'index' + }), {foo: ['baz', 'bar', 'two', 'one']}); + + t.deepEqual(queryString.parse('foo[0]=bar&foo[1]=baz&foo[2]=one&foo[3]=two', { + arrayFormat: 'index' + }), {foo: ['bar', 'baz', 'one', 'two']}); + + t.deepEqual(queryString.parse('foo[3]=three&foo[2]=two&foo[1]=one&foo[0]=zero', { + arrayFormat: 'index' + }), {foo: ['zero', 'one', 'two', 'three']}); + + t.deepEqual(queryString.parse('foo[3]=three&foo[2]=two&foo[1]=one&foo[0]=zero&bat=buz', { + arrayFormat: 'index' + }), {foo: ['zero', 'one', 'two', 'three'], bat: 'buz'}); + + t.deepEqual(queryString.parse('foo[1]=bar&foo[0]=baz', { + arrayFormat: 'index' + }), {foo: ['baz', 'bar']}); + + t.deepEqual(queryString.parse('foo[102]=three&foo[2]=two&foo[1]=one&foo[0]=zero&bat=buz', { + arrayFormat: 'index' + }), {bat: 'buz', foo: ['zero', 'one', 'two', 'three']}); + + t.deepEqual(queryString.parse('foo[102]=three&foo[2]=two&foo[100]=one&foo[0]=zero&bat=buz', { + arrayFormat: 'index' + }), {bat: 'buz', foo: ['zero', 'two', 'one', 'three']}); +}); + +test('circuit parse → stringify', t => { + const original = 'foo[3]=foo&foo[2]&foo[1]=one&foo[0]=&bat=buz'; + const sortedOriginal = 'bat=buz&foo[0]=&foo[1]=one&foo[2]&foo[3]=foo'; + const expected = {bat: 'buz', foo: ['', 'one', null, 'foo']}; + const options = { + arrayFormat: 'index' + }; + + t.deepEqual(queryString.parse(original, options), expected); + + t.is(queryString.stringify(expected, options), sortedOriginal); +}); + +test('circuit original → parse → stringify → sorted original', t => { + const original = 'foo[21474836471]=foo&foo[21474836470]&foo[1]=one&foo[0]=&bat=buz'; + const sortedOriginal = 'bat=buz&foo[0]=&foo[1]=one&foo[2]&foo[3]=foo'; + const options = { + arrayFormat: 'index' + }; + + t.deepEqual(queryString.stringify(queryString.parse(original, options), options), sortedOriginal); +}); + +test('circuit parse → stringify with array commas', t => { + const original = 'c=,a,,&b=&a='; + const sortedOriginal = 'a=&b=&c=,a,,'; + const expected = { + c: ['', 'a', '', ''], + b: '', + a: '' + }; + const options = { + arrayFormat: 'comma' + }; + + t.deepEqual(queryString.parse(original, options), expected); + + t.is(queryString.stringify(expected, options), sortedOriginal); +}); + +test('circuit original → parse → stringify with array commas → sorted original', t => { + const original = 'c=,a,,&b=&a='; + const sortedOriginal = 'a=&b=&c=,a,,'; + const options = { + arrayFormat: 'comma' + }; + + t.deepEqual(queryString.stringify(queryString.parse(original, options), options), sortedOriginal); +}); + +test('decode keys and values', t => { + t.deepEqual(queryString.parse('st%C3%A5le=foo'), {ståle: 'foo'}); + t.deepEqual(queryString.parse('foo=%7B%ab%%7C%de%%7D+%%7Bst%C3%A5le%7D%'), {foo: '{%ab%|%de%} %{ståle}%'}); +}); + +test('disable decoding of keys and values', t => { + t.deepEqual(queryString.parse('tags=postal%20office,burger%2C%20fries%20and%20coke', {decode: false}), {tags: 'postal%20office,burger%2C%20fries%20and%20coke'}); +}); + +test('number value returns as string by default', t => { + t.deepEqual(queryString.parse('foo=1'), {foo: '1'}); +}); + +test('number value returns as number if option is set', t => { + t.deepEqual(queryString.parse('foo=1', {parseNumbers: true}), {foo: 1}); + t.deepEqual(queryString.parse('foo=12.3&bar=123e-1', {parseNumbers: true}), {foo: 12.3, bar: 12.3}); + t.deepEqual(queryString.parse('foo=0x11&bar=12.00', {parseNumbers: true}), {foo: 17, bar: 12}); +}); + +test('NaN value returns as string if option is set', t => { + t.deepEqual(queryString.parse('foo=null', {parseNumbers: true}), {foo: 'null'}); + t.deepEqual(queryString.parse('foo=undefined', {parseNumbers: true}), {foo: 'undefined'}); + t.deepEqual(queryString.parse('foo=100a&bar=100', {parseNumbers: true}), {foo: '100a', bar: 100}); + t.deepEqual(queryString.parse('foo= &bar=', {parseNumbers: true}), {foo: ' ', bar: ''}); +}); + +test('parseNumbers works with arrayFormat', t => { + t.deepEqual(queryString.parse('foo[]=1&foo[]=2&foo[]=3&bar=1', {parseNumbers: true, arrayFormat: 'bracket'}), {foo: [1, 2, 3], bar: 1}); + t.deepEqual(queryString.parse('foo=1,2,a', {parseNumbers: true, arrayFormat: 'comma'}), {foo: [1, 2, 'a']}); + t.deepEqual(queryString.parse('foo=1|2|a', {parseNumbers: true, arrayFormat: 'separator', arrayFormatSeparator: '|'}), {foo: [1, 2, 'a']}); + t.deepEqual(queryString.parse('foo[0]=1&foo[1]=2&foo[2]', {parseNumbers: true, arrayFormat: 'index'}), {foo: [1, 2, null]}); + t.deepEqual(queryString.parse('foo=1&foo=2&foo=3', {parseNumbers: true}), {foo: [1, 2, 3]}); +}); + +test('boolean value returns as string by default', t => { + t.deepEqual(queryString.parse('foo=true'), {foo: 'true'}); +}); + +test('boolean value returns as boolean if option is set', t => { + t.deepEqual(queryString.parse('foo=true', {parseBooleans: true}), {foo: true}); + t.deepEqual(queryString.parse('foo=false&bar=true', {parseBooleans: true}), {foo: false, bar: true}); +}); + +test('parseBooleans works with arrayFormat', t => { + t.deepEqual(queryString.parse('foo[]=true&foo[]=false&foo[]=true&bar=1', {parseBooleans: true, arrayFormat: 'bracket'}), {foo: [true, false, true], bar: '1'}); + t.deepEqual(queryString.parse('foo=true,false,a', {parseBooleans: true, arrayFormat: 'comma'}), {foo: [true, false, 'a']}); + t.deepEqual(queryString.parse('foo[0]=true&foo[1]=false&foo[2]', {parseBooleans: true, arrayFormat: 'index'}), {foo: [true, false, null]}); + t.deepEqual(queryString.parse('foo=true&foo=false&foo=3', {parseBooleans: true}), {foo: [true, false, '3']}); +}); + +test('boolean value returns as boolean and number value as number if both options are set', t => { + t.deepEqual(queryString.parse('foo=true&bar=1.12', {parseNumbers: true, parseBooleans: true}), {foo: true, bar: 1.12}); + t.deepEqual(queryString.parse('foo=16.32&bar=false', {parseNumbers: true, parseBooleans: true}), {foo: 16.32, bar: false}); +}); + +test('parseNumbers and parseBooleans can work with arrayFormat at the same time', t => { + t.deepEqual(queryString.parse('foo=true&foo=false&bar=1.12&bar=2', {parseNumbers: true, parseBooleans: true}), {foo: [true, false], bar: [1.12, 2]}); + t.deepEqual(queryString.parse('foo[]=true&foo[]=false&foo[]=true&bar[]=1&bar[]=2', {parseNumbers: true, parseBooleans: true, arrayFormat: 'bracket'}), {foo: [true, false, true], bar: [1, 2]}); + t.deepEqual(queryString.parse('foo=true,false&bar=1,2', {parseNumbers: true, parseBooleans: true, arrayFormat: 'comma'}), {foo: [true, false], bar: [1, 2]}); + t.deepEqual(queryString.parse('foo[0]=true&foo[1]=false&bar[0]=1&bar[1]=2', {parseNumbers: true, parseBooleans: true, arrayFormat: 'index'}), {foo: [true, false], bar: [1, 2]}); +}); + +test('parse throws TypeError for invalid arrayFormatSeparator', t => { + t.throws(_ => queryString.parse('', {arrayFormatSeparator: ',,'}), { + instanceOf: TypeError + }); + t.throws(_ => queryString.parse('', {arrayFormatSeparator: []}), { + instanceOf: TypeError + }); +}); + +test('query strings having comma encoded and format option as `comma`', t => { + t.deepEqual(queryString.parse('foo=zero%2Cone,two%2Cthree', {arrayFormat: 'comma'}), { + foo: [ + 'zero,one', + 'two,three' + ] + }); +}); + +test('value should not be decoded twice with `arrayFormat` option set as `separator`', t => { + t.deepEqual(queryString.parse('foo=2020-01-01T00:00:00%2B03:00', {arrayFormat: 'separator'}), { + foo: '2020-01-01T00:00:00+03:00' + }); +}); + +// See https://github.com/sindresorhus/query-string/issues/242 +test('value separated by encoded comma will not be parsed as array with `arrayFormat` option set to `comma`', t => { + t.deepEqual(queryString.parse('id=1%2C2%2C3', {arrayFormat: 'comma', parseNumbers: true}), { + id: [1, 2, 3] + }); +}); diff --git a/test/pick.js b/test/pick.js new file mode 100644 index 00000000..0bfaf720 --- /dev/null +++ b/test/pick.js @@ -0,0 +1,21 @@ +import test from 'ava'; +import queryString from '..'; + +test('picks elements in a URL with a filter array', t => { + t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', ['a', 'b']), 'http://example.com/?a=1&b=2#a'); +}); + +test('picks elements in a URL with a filter predicate', t => { + t.is(queryString.pick('http://example.com/?a=1&b=2&c=3#a', (name, value) => { + t.is(typeof name, 'string'); + t.is(typeof value, 'number'); + + return name === 'a'; + }, { + parseNumbers: true + }), 'http://example.com/?a=1#a'); +}); + +test('picks elements in a URL without encoding fragment identifiers', t => { + t.is(queryString.pick('https://example.com?a=b#/home', []), 'https://example.com#/home'); +}); diff --git a/test/properties.js b/test/properties.js new file mode 100644 index 00000000..6d4ace4d --- /dev/null +++ b/test/properties.js @@ -0,0 +1,38 @@ +import deepEqual from 'deep-equal'; +import * as fastCheck from 'fast-check'; +import test from 'ava'; +import queryString from '..'; + +// Valid query parameters must follow: +// - key can be any unicode string (not empty) +// - value must be one of: +// --> any unicode string +// --> null +// --> array containing values defined above (at least two items) +const queryParamsArbitrary = fastCheck.dictionary( + fastCheck.fullUnicodeString(1, 10), + fastCheck.oneof( + fastCheck.fullUnicodeString(), + fastCheck.constant(null), + fastCheck.array(fastCheck.oneof(fastCheck.fullUnicodeString(), fastCheck.constant(null)), 2, 10) + ) +); + +const optionsArbitrary = fastCheck.record({ + arrayFormat: fastCheck.constantFrom('bracket', 'index', 'none'), + strict: fastCheck.boolean(), + encode: fastCheck.constant(true), + sort: fastCheck.constant(false) +}, {withDeletedKeys: true}); + +test('should read correctly from stringified query params', t => { + t.notThrows(() => { + fastCheck.assert( + fastCheck.property( + queryParamsArbitrary, + optionsArbitrary, + (object, options) => deepEqual(queryString.parse(queryString.stringify(object, options), options), object) + ) + ); + }); +}); diff --git a/test/stringify-url.js b/test/stringify-url.js new file mode 100644 index 00000000..2b3c3728 --- /dev/null +++ b/test/stringify-url.js @@ -0,0 +1,58 @@ +import test from 'ava'; +import queryString from '..'; + +test('stringify URL without a query string', t => { + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/'}), 'https://foo.bar/'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/', query: {}}), 'https://foo.bar/'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/#top', query: {}}), 'https://foo.bar/#top'); + t.deepEqual(queryString.stringifyUrl({url: '', query: {}}), ''); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?', query: {}}), 'https://foo.bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?foo=bar', query: {}}), 'https://foo.bar?foo=bar'); +}); + +test('stringify URL with a query string', t => { + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/#top', query: {foo: 'bar'}}), 'https://foo.bar/?foo=bar#top'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar', a: 'b'}}), 'https://foo.bar?a=b&foo=bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?a=b', query: {foo: ['bar', 'baz']}}), 'https://foo.bar?a=b&foo=bar&foo=baz'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar'); +}); + +test('stringify URL with fragment identifier', t => { + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {top: 'foo'}, fragmentIdentifier: 'bar'}), 'https://foo.bar?top=foo#bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: ['bar', 'baz']}, fragmentIdentifier: 'top'}), 'https://foo.bar?foo=bar&foo=baz#top'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/', query: {}, fragmentIdentifier: 'top'}), 'https://foo.bar/#top'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar/#abc', query: {}, fragmentIdentifier: 'top'}), 'https://foo.bar/#top'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}}), 'https://foo.bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {}, fragmentIdentifier: 'foo bar'}), 'https://foo.bar#foo%20bar'); +}); + +test('skipEmptyString:: stringify URL with a query string', t => { + const config = {skipEmptyString: true}; + + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar', baz: ''}}, config), 'https://foo.bar?foo=bar'); + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar', query: {foo: 'bar', baz: ['', 'qux']}}, config), 'https://foo.bar?baz=qux&foo=bar'); +}); + +test('stringify URL from the result of `parseUrl` without query string', t => { + const url = 'https://foo.bar'; + const parsedUrl = queryString.parseUrl(url); + t.deepEqual(queryString.stringifyUrl(parsedUrl), url); +}); + +test('stringify URL from the result of `parseUrl` with query string', t => { + const url = 'https://foo.bar?foo=bar&foo=baz'; + const parsedUrl = queryString.parseUrl(url); + t.deepEqual(queryString.stringifyUrl(parsedUrl), url); +}); + +test('stringify URL from the result of `parseUrl` with query string that contains `=`', t => { + const url = 'https://foo.bar?foo=bar=&foo=baz='; + const parsedUrl = queryString.parseUrl(url); + t.deepEqual(queryString.stringifyUrl(parsedUrl, {encode: false}), url); +}); + +test('stringify URL without sorting existing query params', t => { + t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?C=3&A=1', query: {D: 4, B: 2}}, {sort: false}), 'https://foo.bar?C=3&A=1&D=4&B=2'); +}); diff --git a/test/stringify.js b/test/stringify.js new file mode 100644 index 00000000..c8751eb7 --- /dev/null +++ b/test/stringify.js @@ -0,0 +1,402 @@ +import test from 'ava'; +import queryString from '..'; + +test('stringify', t => { + t.is(queryString.stringify({foo: 'bar'}), 'foo=bar'); + t.is(queryString.stringify({ + foo: 'bar', + bar: 'baz' + }), 'bar=baz&foo=bar'); +}); + +test('different types', t => { + t.is(queryString.stringify(), ''); + t.is(queryString.stringify(0), ''); +}); + +test('URI encode', t => { + t.is(queryString.stringify({'foo bar': 'baz faz'}), 'foo%20bar=baz%20faz'); + t.is(queryString.stringify({'foo bar': 'baz\'faz'}), 'foo%20bar=baz%27faz'); +}); + +test('no encoding', t => { + t.is(queryString.stringify({'foo:bar': 'baz:faz'}, {encode: false}), 'foo:bar=baz:faz'); +}); + +test('handle array value', t => { + t.is(queryString.stringify({ + abc: 'abc', + foo: ['bar', 'baz'] + }), 'abc=abc&foo=bar&foo=baz'); +}); + +test('array order', t => { + t.is(queryString.stringify({ + abc: 'abc', + foo: ['baz', 'bar'] + }), 'abc=abc&foo=baz&foo=bar'); +}); + +test('handle empty array value', t => { + t.is(queryString.stringify({ + abc: 'abc', + foo: [] + }), 'abc=abc'); +}); + +test('should not encode undefined values', t => { + t.is(queryString.stringify({ + abc: undefined, + foo: 'baz' + }), 'foo=baz'); +}); + +test('should encode null values as just a key', t => { + t.is(queryString.stringify({ + 'x y z': null, + abc: null, + foo: 'baz' + }), 'abc&foo=baz&x%20y%20z'); +}); + +test('handle null values in array', t => { + t.is(queryString.stringify({ + foo: null, + bar: [null, 'baz'] + }), 'bar&bar=baz&foo'); +}); + +test('handle undefined values in array', t => { + t.is(queryString.stringify({ + foo: null, + bar: [undefined, 'baz'] + }), 'bar=baz&foo'); +}); + +test('handle undefined and null values in array', t => { + t.is(queryString.stringify({ + foo: null, + bar: [undefined, null, 'baz'] + }), 'bar&bar=baz&foo'); +}); + +test('strict encoding', t => { + t.is(queryString.stringify({foo: '\'bar\''}), 'foo=%27bar%27'); + t.is(queryString.stringify({foo: ['\'bar\'', '!baz']}), 'foo=%27bar%27&foo=%21baz'); +}); + +test('loose encoding', t => { + t.is(queryString.stringify({foo: '\'bar\''}, {strict: false}), 'foo=\'bar\''); + t.is(queryString.stringify({foo: ['\'bar\'', '!baz']}, {strict: false}), 'foo=\'bar\'&foo=!baz'); +}); + +test('array stringify representation with array indexes', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', 'two'] + }, { + arrayFormat: 'index' + }), 'bar[0]=one&bar[1]=two&foo'); +}); + +test('array stringify representation with array brackets', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', 'two'] + }, { + arrayFormat: 'bracket' + }), 'bar[]=one&bar[]=two&foo'); +}); + +test('array stringify representation with array brackets and null value', t => { + t.is(queryString.stringify({ + foo: ['a', null, ''], + bar: [null] + }, { + arrayFormat: 'bracket' + }), 'bar[]&foo[]=a&foo[]&foo[]='); +}); + +test('array stringify representation with array commas', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', 'two'] + }, { + arrayFormat: 'comma' + }), 'bar=one,two&foo'); +}); + +test('array stringify representation with array commas, null & empty string', t => { + t.is(queryString.stringify({ + c: [null, 'a', '', null], + b: [null], + a: [''] + }, { + arrayFormat: 'comma' + }), 'a=&b=&c=,a,,'); +}); + +test('array stringify representation with array commas, null & empty string (skip both)', t => { + t.is(queryString.stringify({ + c: [null, 'a', '', null], + b: [null], + a: [''] + }, { + skipNull: true, + skipEmptyString: true, + arrayFormat: 'comma' + }), 'c=a'); +}); + +test('array stringify representation with array commas and 0 value', t => { + t.is(queryString.stringify({ + foo: ['a', null, 0], + bar: [null] + }, { + arrayFormat: 'comma' + }), 'bar=&foo=a,,0'); +}); + +test('array stringify representation with a bad array format', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', 'two'] + }, { + arrayFormat: 'badinput' + }), 'bar=one&bar=two&foo'); +}); + +test('array stringify representation with array indexes and sparse array', t => { + const fixture = ['one', 'two']; + fixture[10] = 'three'; + t.is(queryString.stringify({bar: fixture}, {arrayFormat: 'index'}), 'bar[0]=one&bar[1]=two&bar[2]=three'); +}); + +test('array stringify representation with brackets and separators with empty array', t => { + t.is(queryString.stringify({ + foo: null, + bar: [] + }, { + arrayFormat: 'bracket-separator' + }), 'bar[]&foo'); +}); + +test('array stringify representation with brackets and separators with single value', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one'] + }, { + arrayFormat: 'bracket-separator' + }), 'bar[]=one&foo'); +}); + +test('array stringify representation with brackets and separators with multiple values', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', 'two', 'three'] + }, { + arrayFormat: 'bracket-separator' + }), 'bar[]=one,two,three&foo'); +}); + +test('array stringify representation with brackets and separators with a single empty string', t => { + t.is(queryString.stringify({ + foo: null, + bar: [''] + }, { + arrayFormat: 'bracket-separator' + }), 'bar[]=&foo'); +}); + +test('array stringify representation with brackets and separators with a multiple empty string', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['', 'two', ''] + }, { + arrayFormat: 'bracket-separator' + }), 'bar[]=,two,&foo'); +}); + +test('array stringify representation with brackets and separators with dropped empty strings', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['', 'two', ''] + }, { + arrayFormat: 'bracket-separator', + skipEmptyString: true + }), 'bar[]=two&foo'); +}); + +test('array stringify representation with brackets and separators with dropped null values', t => { + t.is(queryString.stringify({ + foo: null, + bar: ['one', null, 'three', null, '', 'six'] + }, { + arrayFormat: 'bracket-separator', + skipNull: true + }), 'bar[]=one,three,,six'); +}); + +test('should sort keys in given order', t => { + const fixture = ['c', 'a', 'b']; + const sort = (key1, key2) => fixture.indexOf(key1) - fixture.indexOf(key2); + + t.is(queryString.stringify({a: 'foo', b: 'bar', c: 'baz'}, {sort}), 'c=baz&a=foo&b=bar'); +}); + +test('should not sort when sort is false', t => { + const fixture = { + story: 'a', + patch: 'b', + deployment: 'c', + lat: 10, + lng: 20, + sb: 'd', + sc: 'e', + mn: 'f', + ln: 'g', + nf: 'h', + srs: 'i', + destination: 'g' + }; + t.is(queryString.stringify(fixture, {sort: false}), 'story=a&patch=b&deployment=c&lat=10&lng=20&sb=d&sc=e&mn=f&ln=g&nf=h&srs=i&destination=g'); +}); + +test('should disable sorting', t => { + t.is(queryString.stringify({ + c: 'foo', + b: 'bar', + a: 'baz' + }, { + sort: false + }), 'c=foo&b=bar&a=baz'); +}); + +test('should ignore null when skipNull is set', t => { + t.is(queryString.stringify({ + a: 1, + b: null, + c: 3 + }, { + skipNull: true + }), 'a=1&c=3'); +}); + +test('should ignore emptyString when skipEmptyString is set', t => { + t.is(queryString.stringify({ + a: 1, + b: '', + c: 3 + }, { + skipEmptyString: true + }), 'a=1&c=3'); +}); + +test('should ignore undefined when skipNull is set', t => { + t.is(queryString.stringify({ + a: 1, + b: undefined, + c: 3 + }, { + skipNull: true + }), 'a=1&c=3'); +}); + +test('should ignore both null and undefined when skipNull is set', t => { + t.is(queryString.stringify({ + a: undefined, + b: null + }, { + skipNull: true + }), ''); +}); + +test('should ignore both null and undefined when skipNull is set for arrayFormat', t => { + t.is(queryString.stringify({ + a: [undefined, null, 1, undefined, 2, null], + b: null, + c: 1 + }, { + skipNull: true + }), 'a=1&a=2&c=1'); + + t.is(queryString.stringify({ + a: [undefined, null, 1, undefined, 2, null], + b: null, + c: 1 + }, { + skipNull: true, + arrayFormat: 'bracket' + }), 'a[]=1&a[]=2&c=1'); + + t.is(queryString.stringify({ + a: [undefined, null, 1, undefined, 2, null], + b: null, + c: 1 + }, { + skipNull: true, + arrayFormat: 'comma' + }), 'a=1,2&c=1'); + + t.is(queryString.stringify({ + a: [undefined, null, 1, undefined, 2, null], + b: null, + c: 1 + }, { + skipNull: true, + arrayFormat: 'index' + }), 'a[0]=1&a[1]=2&c=1'); +}); + +test('should ignore empty string when skipEmptyString is set for arrayFormat', t => { + t.is(queryString.stringify({ + a: ['', 1, '', 2], + b: '', + c: 1 + }, { + skipEmptyString: true + }), 'a=1&a=2&c=1'); + + t.is(queryString.stringify({ + a: ['', 1, '', 2], + b: '', + c: 1 + }, { + skipEmptyString: true, + arrayFormat: 'bracket' + }), 'a[]=1&a[]=2&c=1'); + + t.is(queryString.stringify({ + a: ['', 1, '', 2], + b: '', + c: 1 + }, { + skipEmptyString: true, + arrayFormat: 'comma' + }), 'a=1,2&c=1'); + + t.is(queryString.stringify({ + a: ['', 1, '', 2], + b: '', + c: 1 + }, { + skipEmptyString: true, + arrayFormat: 'index' + }), 'a[0]=1&a[1]=2&c=1'); + + t.is(queryString.stringify({ + a: ['', '', '', ''], + c: 1 + }, { + skipEmptyString: true + }), 'c=1'); +}); + +test('stringify throws TypeError for invalid arrayFormatSeparator', t => { + t.throws(_ => queryString.stringify({}, {arrayFormatSeparator: ',,'}), { + instanceOf: TypeError + }); + t.throws(_ => queryString.stringify({}, {arrayFormatSeparator: []}), { + instanceOf: TypeError + }); +});