Repository: sindresorhus/query-string
Pull request #243 (2020-02-25 12:41:57)

Fix value being decoded twice with `arrayFormat` option set to `separator`
fix part of  #242  ( caused by #236 

sindresorhus: > part of 242

What part does this not fix?
sindresorhus: You can use Array#includes instead of indexOf
cheqianxiao: > > part of 242
> 
> What part does this not fix?

it not fix : 
```js
parse(queryString, { arrayFormat: "comma", parseBooleans: true, parseNumbers: true });
// Request: id%28in%29=992232%2C992237%2C992240
// Parsed: 
// {
//    id(in): '992232,992237,992240'
// }
```
sindresorhus: Can you add a [failing test](https://github.com/avajs/ava/blob/master/docs/01-writing-tests.md#failing-tests) for that case that you're not fixing?

Patch:
diff --git a/index.d.ts b/index.d.ts
index 587eaa3..d5012b4 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -283,6 +283,33 @@ export interface StringifyOptions {
 	```
 	*/
 	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;
 }
 
 /**
diff --git a/index.js b/index.js
index 9c63162..0b57b3e 100644
--- a/index.js
+++ b/index.js
@@ -3,12 +3,19 @@ const strictUriEncode = require('strict-uri-encode');
 const decodeComponent = require('decode-uri-component');
 const splitOnFirst = require('split-on-first');
 
+const isNullOrUndefined = value => value === null || value === undefined;
+
 function encoderForArrayFormat(options) {
 	switch (options.arrayFormat) {
 		case 'index':
 			return key => (result, value) => {
 				const index = result.length;
-				if (value === undefined || (options.skipNull && value === null)) {
+
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -24,7 +31,11 @@ function encoderForArrayFormat(options) {
 
 		case 'bracket':
 			return key => (result, value) => {
-				if (value === undefined || (options.skipNull && value === null)) {
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -51,7 +62,11 @@ function encoderForArrayFormat(options) {
 
 		default:
 			return key => (result, value) => {
-				if (value === undefined || (options.skipNull && value === null)) {
+				if (
+					value === undefined ||
+					(options.skipNull && value === null) ||
+					(options.skipEmptyString && value === '')
+				) {
 					return result;
 				}
 
@@ -231,7 +246,7 @@ function parse(input, options) {
 
 		// Missing `=` should be `null`:
 		// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
-		value = value === undefined ? null : options.arrayFormat === 'comma' ? value : decode(value, options);
+		value = value === undefined ? null : ['comma', 'separator'].includes(options.arrayFormat) ? value : decode(value, options);
 		formatter(decode(key, options), value, ret);
 	}
 
@@ -280,14 +295,18 @@ exports.stringify = (object, options) => {
 
 	validateArrayFormatSeparator(options.arrayFormatSeparator);
 
+	const shouldFilter = key => (
+		(options.skipNull && isNullOrUndefined(object[key])) ||
+		(options.skipEmptyString && object[key] === '')
+	);
+
 	const formatter = encoderForArrayFormat(options);
 
-	const objectCopy = Object.assign({}, object);
-	if (options.skipNull) {
-		for (const key of Object.keys(objectCopy)) {
-			if (objectCopy[key] === undefined || objectCopy[key] === null) {
-				delete objectCopy[key];
-			}
+	const objectCopy = {};
+
+	for (const key of Object.keys(object)) {
+		if (!shouldFilter(key)) {
+			objectCopy[key] = object[key];
 		}
 	}
 
diff --git a/package.json b/package.json
index a35accd..2265152 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "query-string",
-	"version": "6.11.1",
+	"version": "6.12.0",
 	"description": "Parse and stringify URL query strings",
 	"license": "MIT",
 	"repository": "sindresorhus/query-string",
diff --git a/readme.md b/readme.md
index 3dd2ea0..d6d2bb0 100644
--- a/readme.md
+++ b/readme.md
@@ -304,6 +304,31 @@ queryString.stringify({a: undefined, b: null}, {
 //=> ''
 ```
 
+##### 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()`.


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index a4cb36d..19a6817 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -23,6 +23,7 @@ expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'none'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'comma'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {sort: false}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {skipNull: true}));
+expectType<string>(queryString.stringify({foo: 'bar'}, {skipEmptyString: true}));
 const order = ['c', 'a', 'b'];
 expectType<string>(
 	queryString.stringify(
diff --git a/test/parse.js b/test/parse.js
index 3c442e7..8964144 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -310,3 +310,16 @@ test('query strings having comma encoded and format option as `comma`', t => {
 		]
 	});
 });
+
+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.failing('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/stringify-url.js b/test/stringify-url.js
index 2bde443..933af7a 100644
--- a/test/stringify-url.js
+++ b/test/stringify-url.js
@@ -19,6 +19,13 @@ test('stringify URL with a query string', t => {
 	t.deepEqual(queryString.stringifyUrl({url: 'https://foo.bar?foo=baz', query: {foo: 'bar'}}), 'https://foo.bar?foo=bar');
 });
 
+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);
diff --git a/test/stringify.js b/test/stringify.js
index 147d2e2..5c3487b 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -204,6 +204,16 @@ test('should ignore null when skipNull is set', t => {
 	}), '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,
@@ -260,6 +270,50 @@ test('should ignore both null and undefined when skipNull is set for arrayFormat
 	}), '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