@nocobase/plugin-multi-keyword-filter 2.2.17 → 2.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/client/103.b39ee70786cb13a7.js +10 -0
  2. package/dist/client/index.js +1 -1
  3. package/dist/client-v2/103.dfed67c95bcd8a1d.js +10 -0
  4. package/dist/client-v2/index.js +1 -1
  5. package/dist/externalVersion.js +4 -4
  6. package/dist/node_modules/qs/.editorconfig +4 -1
  7. package/dist/node_modules/qs/.github/SECURITY.md +11 -0
  8. package/dist/node_modules/qs/.github/THREAT_MODEL.md +78 -0
  9. package/dist/node_modules/qs/dist/qs.js +90 -2036
  10. package/dist/node_modules/qs/lib/index.js +1 -1
  11. package/dist/node_modules/qs/lib/parse.js +215 -66
  12. package/dist/node_modules/qs/lib/stringify.js +90 -32
  13. package/dist/node_modules/qs/lib/utils.js +189 -53
  14. package/dist/node_modules/qs/node_modules/es-define-property/.eslintrc +13 -0
  15. package/dist/node_modules/qs/node_modules/es-define-property/.github/FUNDING.yml +12 -0
  16. package/dist/node_modules/qs/node_modules/es-define-property/.nycrc +9 -0
  17. package/dist/node_modules/qs/node_modules/es-define-property/index.d.ts +3 -0
  18. package/dist/node_modules/qs/node_modules/es-define-property/index.js +14 -0
  19. package/dist/node_modules/qs/node_modules/es-define-property/package.json +81 -0
  20. package/dist/node_modules/qs/node_modules/es-define-property/test/index.js +56 -0
  21. package/dist/node_modules/qs/node_modules/side-channel/.editorconfig +9 -0
  22. package/dist/node_modules/qs/node_modules/side-channel/.eslintrc +12 -0
  23. package/dist/node_modules/qs/node_modules/side-channel/.github/FUNDING.yml +12 -0
  24. package/dist/node_modules/qs/node_modules/side-channel/.nycrc +13 -0
  25. package/dist/node_modules/qs/node_modules/side-channel/index.d.ts +14 -0
  26. package/dist/node_modules/qs/node_modules/side-channel/index.js +46 -0
  27. package/dist/node_modules/qs/node_modules/side-channel/package.json +85 -0
  28. package/dist/node_modules/qs/node_modules/side-channel/test/index.js +120 -0
  29. package/dist/node_modules/qs/node_modules/side-channel-list/.editorconfig +9 -0
  30. package/dist/node_modules/qs/node_modules/side-channel-list/.eslintrc +11 -0
  31. package/dist/node_modules/qs/node_modules/side-channel-list/.github/FUNDING.yml +12 -0
  32. package/dist/node_modules/qs/node_modules/side-channel-list/.nycrc +13 -0
  33. package/dist/node_modules/qs/node_modules/side-channel-list/index.d.ts +13 -0
  34. package/dist/node_modules/qs/node_modules/side-channel-list/index.js +111 -0
  35. package/dist/node_modules/qs/node_modules/side-channel-list/list.d.ts +14 -0
  36. package/dist/node_modules/qs/node_modules/side-channel-list/package.json +77 -0
  37. package/dist/node_modules/qs/node_modules/side-channel-list/test/index.js +154 -0
  38. package/dist/node_modules/qs/package.json +1 -1
  39. package/dist/node_modules/qs/test/empty-keys-cases.js +261 -31
  40. package/dist/node_modules/qs/test/parse.js +1281 -57
  41. package/dist/node_modules/qs/test/stringify.js +851 -58
  42. package/dist/node_modules/qs/test/utils.js +505 -3
  43. package/package.json +5 -2
  44. package/dist/client/103.513149b773e262dc.js +0 -10
  45. package/dist/client-v2/103.8bcbf628d0af8457.js +0 -10
  46. package/dist/node_modules/qs/.eslintrc +0 -38
@@ -6,8 +6,10 @@ var utils = require('../lib/utils');
6
6
  var iconv = require('iconv-lite');
7
7
  var SaferBuffer = require('safer-buffer').Buffer;
8
8
  var hasSymbols = require('has-symbols');
9
+ var mockProperty = require('mock-property');
9
10
  var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
10
- var hasBigInt = typeof BigInt === 'function';
11
+ var hasProto = require('has-proto')();
12
+ var hasBigInt = require('has-bigints')();
11
13
 
12
14
  test('stringify()', function (t) {
13
15
  t.test('stringifies a querystring object', function (st) {
@@ -64,6 +66,276 @@ test('stringify()', function (t) {
64
66
  st.end();
65
67
  });
66
68
 
69
+ t.test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function (st) {
70
+ st.equal(
71
+ qs.stringify(
72
+ { 'name.obj': { first: 'John', last: 'Doe' } },
73
+ { allowDots: false, encodeDotInKeys: false }
74
+ ),
75
+ 'name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe',
76
+ 'with allowDots false and encodeDotInKeys false'
77
+ );
78
+ st.equal(
79
+ qs.stringify(
80
+ { 'name.obj': { first: 'John', last: 'Doe' } },
81
+ { allowDots: true, encodeDotInKeys: false }
82
+ ),
83
+ 'name.obj.first=John&name.obj.last=Doe',
84
+ 'with allowDots true and encodeDotInKeys false'
85
+ );
86
+ st.equal(
87
+ qs.stringify(
88
+ { 'name.obj': { first: 'John', last: 'Doe' } },
89
+ { allowDots: false, encodeDotInKeys: true }
90
+ ),
91
+ 'name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe',
92
+ 'with allowDots false and encodeDotInKeys true'
93
+ );
94
+ st.equal(
95
+ qs.stringify(
96
+ { 'name.obj': { first: 'John', last: 'Doe' } },
97
+ { allowDots: true, encodeDotInKeys: true }
98
+ ),
99
+ 'name%252Eobj.first=John&name%252Eobj.last=Doe',
100
+ 'with allowDots true and encodeDotInKeys true'
101
+ );
102
+
103
+ st.equal(
104
+ qs.stringify(
105
+ { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
106
+ { allowDots: false, encodeDotInKeys: false }
107
+ ),
108
+ 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe',
109
+ 'with allowDots false and encodeDotInKeys false'
110
+ );
111
+ st.equal(
112
+ qs.stringify(
113
+ { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
114
+ { allowDots: true, encodeDotInKeys: false }
115
+ ),
116
+ 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
117
+ 'with allowDots false and encodeDotInKeys false'
118
+ );
119
+ st.equal(
120
+ qs.stringify(
121
+ { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
122
+ { allowDots: false, encodeDotInKeys: true }
123
+ ),
124
+ 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe',
125
+ 'with allowDots false and encodeDotInKeys true'
126
+ );
127
+ st.equal(
128
+ qs.stringify(
129
+ { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
130
+ { allowDots: true, encodeDotInKeys: true }
131
+ ),
132
+ 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
133
+ 'with allowDots true and encodeDotInKeys true'
134
+ );
135
+
136
+ st.end();
137
+ });
138
+
139
+ t.test('encodes dot in key of object with a primitive value when encodeDotInKeys and allowDots is provided', function (st) {
140
+ st.equal(
141
+ qs.stringify(
142
+ { 'name.obj': 'John' },
143
+ { allowDots: true, encodeDotInKeys: true }
144
+ ),
145
+ 'name%252Eobj=John',
146
+ 'with allowDots true and encodeDotInKeys true'
147
+ );
148
+ st.equal(
149
+ qs.stringify(
150
+ { 'name.obj': 'John' },
151
+ { allowDots: false, encodeDotInKeys: true }
152
+ ),
153
+ 'name%252Eobj=John',
154
+ 'with allowDots false and encodeDotInKeys true'
155
+ );
156
+ st.equal(
157
+ qs.stringify(
158
+ { 'name.obj': 'John' },
159
+ { encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
160
+ ),
161
+ 'name%2Eobj=John',
162
+ 'with encodeValuesOnly true'
163
+ );
164
+
165
+ // With `encodeValuesOnly` the key encoder is skipped, so the dot is encoded once
166
+ // (`%2E`) rather than twice. That single `%2E` decodes back to a structural dot, so
167
+ // unlike the default output below this does not round-trip to the original dotted key
168
+ // (a pre-existing `encodeValuesOnly` + `encodeDotInKeys` limitation, not new here).
169
+ st.deepEqual(
170
+ qs.parse(
171
+ qs.stringify(
172
+ { 'name.obj': 'John' },
173
+ { encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true }
174
+ ),
175
+ { allowDots: true, decodeDotInKeys: true }
176
+ ),
177
+ { name: { obj: 'John' } },
178
+ 'with encodeValuesOnly the single-encoded dot decodes structurally (does not round-trip)'
179
+ );
180
+
181
+ st.deepEqual(
182
+ qs.parse(
183
+ qs.stringify({ 'name.obj': 'John' }, { allowDots: true, encodeDotInKeys: true }),
184
+ { allowDots: true, decodeDotInKeys: true }
185
+ ),
186
+ { 'name.obj': 'John' },
187
+ 'round-trips a dotted key with a primitive value'
188
+ );
189
+
190
+ st.equal(
191
+ qs.stringify({ 'a.b': null }, { allowDots: true, encodeDotInKeys: true, strictNullHandling: true }),
192
+ 'a%252Eb',
193
+ 'encodes a dotted key with a null value'
194
+ );
195
+ st.equal(
196
+ qs.stringify({ 'a.b': '1', 'c.d': '2' }, { allowDots: true, encodeDotInKeys: true }),
197
+ 'a%252Eb=1&c%252Ed=2',
198
+ 'encodes dots in every top-level key, not just the first'
199
+ );
200
+ st.equal(
201
+ qs.stringify({ 'a.b.c': 'x' }, { allowDots: true, encodeDotInKeys: true }),
202
+ 'a%252Eb%252Ec=x',
203
+ 'encodes every dot in a key'
204
+ );
205
+ var primitiveKey = qs.stringify({ 'a.b': 'c' }, { allowDots: true, encodeDotInKeys: true }).split('=')[0];
206
+ st.equal(
207
+ qs.stringify({ 'a.b': { x: 'c' } }, { allowDots: true, encodeDotInKeys: true }).indexOf(primitiveKey + '.'),
208
+ 0,
209
+ 'a top-level dotted key encodes identically for primitive and object values'
210
+ );
211
+
212
+ st.end();
213
+ });
214
+
215
+ // Characterization of the CURRENT (buggy) behavior: with `allowDots` + `encodeDotInKeys`,
216
+ // `stringify` percent-encodes the structural `.` separators that `allowDots` inserts between
217
+ // nested keys, not just the literal dots inside a key, which breaks the stringify -> parse
218
+ // round-trip. These assertions pin that behavior so it cannot change silently: the fix in
219
+ // https://github.com/ljharb/qs/pull/564 changes every output below, so landing that fix MUST
220
+ // update this test (that is the point — it forces the behavior change to be explicit).
221
+ t.test('[characterization] encodeDotInKeys currently over-encodes structural separator dots (pre-#564)', function (st) {
222
+ st.equal(
223
+ qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
224
+ 'a%252Eb.c=d',
225
+ 'over-encodes the a->b separator (#564: should become a.b.c=d)'
226
+ );
227
+ st.equal(
228
+ qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true, encodeDotInKeys: true }),
229
+ 'a%252Eb%252Ec.d=e',
230
+ 'clobbers every separator but the last (#564: should become a.b.c.d=e)'
231
+ );
232
+ st.equal(
233
+ qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
234
+ 'a%252Eb%252Ec.d=e',
235
+ 'top-level dotted key: deeper separators also get encoded (#564: should become a%252Eb.c.d=e)'
236
+ );
237
+ st.equal(
238
+ qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
239
+ 'a%252Eb%252Ec.d=e',
240
+ 'nested dotted key + object value: the separator around it is encoded (#564: should become a.b%252Ec.d=e)'
241
+ );
242
+ st.equal(
243
+ qs.stringify({ a: { b: { 'c.d': { e: 'f' } } } }, { allowDots: true, encodeDotInKeys: true }),
244
+ 'a%252Eb%252Ec%252Ed.e=f',
245
+ 'deeply nested dotted key: all separators encoded (#564: should become a.b.c%252Ed.e=f)'
246
+ );
247
+
248
+ st.equal(
249
+ qs.stringify({ a: { b: [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
250
+ 'a%252Eb%5B0%5D=1&a%252Eb%5B1%5D=2',
251
+ 'array value under nested keys: separator encoded (#564: should become a.b%5B0%5D=1&a.b%5B1%5D=2)'
252
+ );
253
+ st.equal(
254
+ qs.stringify({ a: { 'b.c': [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
255
+ 'a%252Eb%252Ec%5B0%5D=1&a%252Eb%252Ec%5B1%5D=2',
256
+ 'array value under nested dotted key (#564: should become a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2)'
257
+ );
258
+
259
+ st.equal(
260
+ qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
261
+ 'a%5Bb%252Ec%5D%5Bd%5D=e',
262
+ 'allowDots:false object value encodes the nested-key dot, unlike the primitive-value case (#564: should become a%5Bb.c%5D%5Bd%5D=e)'
263
+ );
264
+
265
+ st.deepEqual(
266
+ qs.parse(
267
+ qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
268
+ { allowDots: true, decodeDotInKeys: true }
269
+ ),
270
+ { 'a.b': { c: 'd' } },
271
+ 'the over-encoding breaks the round-trip: {a:{b:{c}}} comes back as {"a.b":{c}} (#564 restores it)'
272
+ );
273
+
274
+ st.end();
275
+ });
276
+
277
+ t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
278
+ st.equal(
279
+ qs.stringify(
280
+ { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
281
+ { encodeDotInKeys: true }
282
+ ),
283
+ 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
284
+ 'with allowDots undefined and encodeDotInKeys true'
285
+ );
286
+ st.end();
287
+ });
288
+
289
+ t.test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function (st) {
290
+ st.equal(
291
+ qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, {
292
+ encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true
293
+ }),
294
+ 'name%2Eobj.first=John&name%2Eobj.last=Doe'
295
+ );
296
+
297
+ st.equal(
298
+ qs.stringify({ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }),
299
+ 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'
300
+ );
301
+
302
+ st.end();
303
+ });
304
+
305
+ t.test('throws when `commaRoundTrip` is not a boolean', function (st) {
306
+ st['throws'](
307
+ function () { qs.stringify({}, { commaRoundTrip: 'not a boolean' }); },
308
+ TypeError,
309
+ 'throws when `commaRoundTrip` is not a boolean'
310
+ );
311
+
312
+ st.end();
313
+ });
314
+
315
+ t.test('throws when `encodeDotInKeys` is not a boolean', function (st) {
316
+ st['throws'](
317
+ function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); },
318
+ TypeError
319
+ );
320
+
321
+ st['throws'](
322
+ function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); },
323
+ TypeError
324
+ );
325
+
326
+ st['throws'](
327
+ function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); },
328
+ TypeError
329
+ );
330
+
331
+ st['throws'](
332
+ function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); },
333
+ TypeError
334
+ );
335
+
336
+ st.end();
337
+ });
338
+
67
339
  t.test('adds query prefix', function (st) {
68
340
  st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
69
341
  st.end();
@@ -87,7 +359,7 @@ test('stringify()', function (t) {
87
359
  st.end();
88
360
  });
89
361
 
90
- t.test('stringifies a nested object with dots notation', function (st) {
362
+ t.test('`allowDots` option: stringifies a nested object with dots notation', function (st) {
91
363
  st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
92
364
  st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
93
365
  st.end();
@@ -109,6 +381,11 @@ test('stringify()', function (t) {
109
381
  'a=b%2Cc%2Cd',
110
382
  'comma => comma'
111
383
  );
384
+ st.equal(
385
+ qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }),
386
+ 'a=b%2Cc%2Cd',
387
+ 'comma round trip => comma'
388
+ );
112
389
  st.equal(
113
390
  qs.stringify({ a: ['b', 'c', 'd'] }),
114
391
  'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
@@ -117,18 +394,108 @@ test('stringify()', function (t) {
117
394
  st.end();
118
395
  });
119
396
 
120
- t.test('omits nulls when asked', function (st) {
121
- st.equal(qs.stringify({ a: 'b', c: null }, { skipNulls: true }), 'a=b');
122
- st.end();
123
- });
397
+ t.test('`skipNulls` option', function (st) {
398
+ st.equal(
399
+ qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
400
+ 'a=b',
401
+ 'omits nulls when asked'
402
+ );
403
+
404
+ st.equal(
405
+ qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }),
406
+ 'a%5Bb%5D=c',
407
+ 'omits nested nulls when asked'
408
+ );
124
409
 
125
- t.test('omits nested nulls when asked', function (st) {
126
- st.equal(qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), 'a%5Bb%5D=c');
127
410
  st.end();
128
411
  });
129
412
 
130
413
  t.test('omits array indices when asked', function (st) {
131
414
  st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
415
+
416
+ st.end();
417
+ });
418
+
419
+ t.test('omits object key/value pair when value is empty array', function (st) {
420
+ st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
421
+
422
+ st.end();
423
+ });
424
+
425
+ t.test('should not omit object key/value pair when value is empty array and when asked', function (st) {
426
+ st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
427
+ st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz');
428
+ st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz');
429
+
430
+ st.end();
431
+ });
432
+
433
+ t.test('should throw when allowEmptyArrays is not of type boolean', function (st) {
434
+ st['throws'](
435
+ function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); },
436
+ TypeError
437
+ );
438
+
439
+ st['throws'](
440
+ function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); },
441
+ TypeError
442
+ );
443
+
444
+ st['throws'](
445
+ function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); },
446
+ TypeError
447
+ );
448
+
449
+ st['throws'](
450
+ function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); },
451
+ TypeError
452
+ );
453
+
454
+ st.end();
455
+ });
456
+
457
+ t.test('allowEmptyArrays + strictNullHandling', function (st) {
458
+ st.equal(
459
+ qs.stringify(
460
+ { testEmptyArray: [] },
461
+ { strictNullHandling: true, allowEmptyArrays: true }
462
+ ),
463
+ 'testEmptyArray[]'
464
+ );
465
+
466
+ st.end();
467
+ });
468
+
469
+ t.test('allowEmptyArrays only short-circuits arrays with no own keys', function (st) {
470
+ st.equal(
471
+ qs.stringify({ a: [] }, { allowEmptyArrays: true }),
472
+ 'a[]',
473
+ 'a genuinely empty array is still emitted as `[]`'
474
+ );
475
+
476
+ var withKey = [];
477
+ withKey.extra = 'x';
478
+ st.equal(
479
+ qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
480
+ 'a%5Bextra%5D=x',
481
+ 'an own key on an empty array is serialized rather than dropped'
482
+ );
483
+ st.equal(
484
+ qs.stringify({ a: withKey }, { allowEmptyArrays: true }),
485
+ qs.stringify({ a: withKey }),
486
+ 'the allowEmptyArrays fast path matches the default path when the array has own keys'
487
+ );
488
+
489
+ var container = {};
490
+ var cyclic = [];
491
+ cyclic.back = container;
492
+ container.a = cyclic;
493
+ st['throws'](
494
+ function () { qs.stringify(container, { allowEmptyArrays: true }); },
495
+ RangeError,
496
+ 'a cycle through an empty array with an own key is still detected'
497
+ );
498
+
132
499
  st.end();
133
500
  });
134
501
 
@@ -156,6 +523,7 @@ test('stringify()', function (t) {
156
523
  s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c&a[1]=d');
157
524
  s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c&a[]=d');
158
525
  s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c,d');
526
+ s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c,d');
159
527
  s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d');
160
528
 
161
529
  s2t.end();
@@ -165,6 +533,9 @@ test('stringify()', function (t) {
165
533
  s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c%2Cd,e');
166
534
  s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce');
167
535
 
536
+ s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd,e');
537
+ s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd%2Ce');
538
+
168
539
  s2t.end();
169
540
  });
170
541
 
@@ -255,36 +626,44 @@ test('stringify()', function (t) {
255
626
 
256
627
  t.test('stringifies an object inside an array', function (st) {
257
628
  st.equal(
258
- qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices' }),
259
- 'a%5B0%5D%5Bb%5D=c', // a[0][b]=c
260
- 'indices => brackets'
629
+ qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
630
+ 'a[0][b]=c',
631
+ 'indices => indices'
632
+ );
633
+ st.equal(
634
+ qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
635
+ 'a[b]=c',
636
+ 'repeat => repeat'
261
637
  );
262
638
  st.equal(
263
- qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets' }),
264
- 'a%5B%5D%5Bb%5D=c', // a[][b]=c
639
+ qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
640
+ 'a[][b]=c',
265
641
  'brackets => brackets'
266
642
  );
267
643
  st.equal(
268
- qs.stringify({ a: [{ b: 'c' }] }),
269
- 'a%5B0%5D%5Bb%5D=c',
644
+ qs.stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }),
645
+ 'a[0][b]=c',
270
646
  'default => indices'
271
647
  );
272
648
 
273
649
  st.equal(
274
- qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices' }),
275
- 'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
650
+ qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
651
+ 'a[0][b][c][0]=1',
276
652
  'indices => indices'
277
653
  );
278
-
279
654
  st.equal(
280
- qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets' }),
281
- 'a%5B%5D%5Bb%5D%5Bc%5D%5B%5D=1',
655
+ qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
656
+ 'a[b][c]=1',
657
+ 'repeat => repeat'
658
+ );
659
+ st.equal(
660
+ qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
661
+ 'a[][b][c][]=1',
282
662
  'brackets => brackets'
283
663
  );
284
-
285
664
  st.equal(
286
- qs.stringify({ a: [{ b: { c: [1] } }] }),
287
- 'a%5B0%5D%5Bb%5D%5Bc%5D%5B0%5D=1',
665
+ qs.stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }),
666
+ 'a[0][b][c][0]=1',
288
667
  'default => indices'
289
668
  );
290
669
 
@@ -386,17 +765,17 @@ test('stringify()', function (t) {
386
765
  st.end();
387
766
  });
388
767
 
389
- t.test('uses indices notation for arrays when no arrayFormat=indices', function (st) {
768
+ t.test('uses indices notation for arrays when arrayFormat=indices', function (st) {
390
769
  st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
391
770
  st.end();
392
771
  });
393
772
 
394
- t.test('uses repeat notation for arrays when no arrayFormat=repeat', function (st) {
773
+ t.test('uses repeat notation for arrays when arrayFormat=repeat', function (st) {
395
774
  st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
396
775
  st.end();
397
776
  });
398
777
 
399
- t.test('uses brackets notation for arrays when no arrayFormat=brackets', function (st) {
778
+ t.test('uses brackets notation for arrays when arrayFormat=brackets', function (st) {
400
779
  st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
401
780
  st.end();
402
781
  });
@@ -443,10 +822,51 @@ test('stringify()', function (t) {
443
822
  st.end();
444
823
  });
445
824
 
446
- t.test('stringifies a null object', { skip: !Object.create }, function (st) {
447
- var obj = Object.create(null);
448
- obj.a = 'b';
449
- st.equal(qs.stringify(obj), 'a=b');
825
+ t.test('does not crash on null/undefined entries in arrayFormat=comma with encodeValuesOnly', function (st) {
826
+ st.doesNotThrow(
827
+ function () { qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
828
+ 'does not pass a raw null array entry to the encoder'
829
+ );
830
+ st.doesNotThrow(
831
+ function () { qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
832
+ 'does not pass a raw undefined array entry to the encoder'
833
+ );
834
+ st.doesNotThrow(
835
+ function () { qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }); },
836
+ 'does not crash on a single-null array'
837
+ );
838
+
839
+ st.equal(
840
+ qs.stringify({ a: [null, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
841
+ 'a=,b',
842
+ 'null entry joins as empty, comma stays unencoded under encodeValuesOnly'
843
+ );
844
+ st.equal(
845
+ qs.stringify({ a: [undefined, 'b'] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
846
+ 'a=,b',
847
+ 'undefined entry joins as empty, comma stays unencoded under encodeValuesOnly'
848
+ );
849
+ st.equal(
850
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true }),
851
+ 'a=',
852
+ 'single-null array stringifies as empty value'
853
+ );
854
+ st.equal(
855
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, strictNullHandling: true }),
856
+ 'a',
857
+ 'strictNullHandling drops the equals sign for a single-null array'
858
+ );
859
+ st.equal(
860
+ qs.stringify({ a: [null] }, { arrayFormat: 'comma', encodeValuesOnly: true, skipNulls: true }),
861
+ '',
862
+ 'skipNulls drops a single-null array entirely'
863
+ );
864
+
865
+ st.end();
866
+ });
867
+
868
+ t.test('stringifies a null object', { skip: !hasProto }, function (st) {
869
+ st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
450
870
  st.end();
451
871
  });
452
872
 
@@ -458,11 +878,8 @@ test('stringify()', function (t) {
458
878
  st.end();
459
879
  });
460
880
 
461
- t.test('stringifies an object with a null object as a child', { skip: !Object.create }, function (st) {
462
- var obj = { a: Object.create(null) };
463
-
464
- obj.a.b = 'c';
465
- st.equal(qs.stringify(obj), 'a%5Bb%5D=c');
881
+ t.test('stringifies an object with a null object as a child', { skip: !hasProto }, function (st) {
882
+ st.equal(qs.stringify({ a: { __proto__: null, b: 'c' } }), 'a%5Bb%5D=c');
466
883
  st.end();
467
884
  });
468
885
 
@@ -493,10 +910,11 @@ test('stringify()', function (t) {
493
910
  });
494
911
 
495
912
  t.test('skips properties that are part of the object prototype', function (st) {
496
- Object.prototype.crash = 'test';
913
+ st.intercept(Object.prototype, 'crash', { value: 'test' });
914
+
497
915
  st.equal(qs.stringify({ a: 'b' }), 'a=b');
498
916
  st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
499
- delete Object.prototype.crash;
917
+
500
918
  st.end();
501
919
  });
502
920
 
@@ -514,16 +932,45 @@ test('stringify()', function (t) {
514
932
  st.end();
515
933
  });
516
934
 
935
+ t.test('does not throw on a non-callable `constructor.isBuffer` key', function (st) {
936
+ var input = { a: { constructor: { isBuffer: 'x' } } };
937
+ st.doesNotThrow(function () { qs.stringify(input); });
938
+ st.equal(qs.stringify(input), 'a%5Bconstructor%5D%5BisBuffer%5D=x');
939
+ st.equal(qs.stringify({ a: { constructor: { isBuffer: { b: 'c' } } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5Bb%5D=c');
940
+ st.equal(qs.stringify({ a: { constructor: { isBuffer: ['b'] } } }), 'a%5Bconstructor%5D%5BisBuffer%5D%5B0%5D=b');
941
+ st.equal(qs.stringify({ a: { constructor: { isBuffer: true } } }), 'a%5Bconstructor%5D%5BisBuffer%5D=true');
942
+ st.end();
943
+ });
944
+
945
+ t.test('round-trips a parsed `constructor[isBuffer]` key', function (st) {
946
+ var str = 'x%5Bconstructor%5D%5BisBuffer%5D=y';
947
+
948
+ var plain = qs.parse(str, { plainObjects: true });
949
+ st.deepEqual(plain, { __proto__: null, x: { __proto__: null, constructor: { __proto__: null, isBuffer: 'y' } } }, 'plainObjects keeps the key');
950
+ st.doesNotThrow(function () { qs.stringify(plain); });
951
+ st.equal(qs.stringify(plain), str, 'plainObjects round-trips');
952
+
953
+ var proto = qs.parse(str, { allowPrototypes: true });
954
+ st.deepEqual(proto, { x: { constructor: { isBuffer: 'y' } } }, 'allowPrototypes keeps the key');
955
+ st.doesNotThrow(function () { qs.stringify(proto); });
956
+ st.equal(qs.stringify(proto), str, 'allowPrototypes round-trips');
957
+
958
+ st.equal(qs.stringify(qs.parse(str)), '', 'default options drop the key');
959
+ st.end();
960
+ });
961
+
517
962
  t.test('stringifies an object using an alternative delimiter', function (st) {
518
963
  st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
519
964
  st.end();
520
965
  });
521
966
 
522
967
  t.test('does not blow up when Buffer global is missing', function (st) {
523
- var tempBuffer = global.Buffer;
524
- delete global.Buffer;
968
+ var restore = mockProperty(global, 'Buffer', { 'delete': true });
969
+
525
970
  var result = qs.stringify({ a: 'b', c: 'd' });
526
- global.Buffer = tempBuffer;
971
+
972
+ restore();
973
+
527
974
  st.equal(result, 'a=b&c=d');
528
975
  st.end();
529
976
  });
@@ -572,9 +1019,17 @@ test('stringify()', function (t) {
572
1019
  };
573
1020
 
574
1021
  st.equal(
575
- qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true }),
1022
+ qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
576
1023
  'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23'
577
1024
  );
1025
+ st.equal(
1026
+ qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
1027
+ 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23'
1028
+ );
1029
+ st.equal(
1030
+ qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }),
1031
+ 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23'
1032
+ );
578
1033
 
579
1034
  st.end();
580
1035
  });
@@ -611,6 +1066,35 @@ test('stringify()', function (t) {
611
1066
  st.end();
612
1067
  });
613
1068
 
1069
+ t.test('skips null/undefined entries in filter=array', function (st) {
1070
+ st.doesNotThrow(
1071
+ function () { qs.stringify({ a: 'b', undefined: 'x' }, { filter: ['a', undefined] }); },
1072
+ 'does not pass a raw undefined filter entry to the encoder'
1073
+ );
1074
+ st.doesNotThrow(
1075
+ function () { qs.stringify({ a: 'b', 'null': 'x' }, { filter: ['a', null] }); },
1076
+ 'does not pass a raw null filter entry to the encoder'
1077
+ );
1078
+
1079
+ st.equal(
1080
+ qs.stringify({ a: 'b', undefined: 'x', c: 'd' }, { filter: ['a', undefined, 'c'] }),
1081
+ 'a=b&c=d',
1082
+ 'undefined filter entry is skipped, remaining keys are kept'
1083
+ );
1084
+ st.equal(
1085
+ qs.stringify({ a: 'b', 'null': 'x', c: 'd' }, { filter: ['a', null, 'c'] }),
1086
+ 'a=b&c=d',
1087
+ 'null filter entry is skipped, remaining keys are kept'
1088
+ );
1089
+ st.equal(
1090
+ qs.stringify({ a: 'b', 'null': 'x' }, { filter: [null] }),
1091
+ '',
1092
+ 'filter array containing only null yields empty string'
1093
+ );
1094
+
1095
+ st.end();
1096
+ });
1097
+
614
1098
  t.test('supports custom representations when filter=function', function (st) {
615
1099
  var calls = 0;
616
1100
  var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
@@ -633,6 +1117,66 @@ test('stringify()', function (t) {
633
1117
  st.end();
634
1118
  });
635
1119
 
1120
+ t.test('serializes Date values unless filter transforms them', function (st) {
1121
+ var obj = {
1122
+ a: new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260)),
1123
+ b: [new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260))]
1124
+ };
1125
+ var iso = '2023-03-07T09%3A02%3A41.260Z';
1126
+ st.equal(
1127
+ qs.stringify(obj, { filter: function (prefix, value) { return value; } }),
1128
+ 'a=' + iso + '&b%5B0%5D=' + iso,
1129
+ 'Dates returned unchanged by a filter are serialized'
1130
+ );
1131
+ st.equal(
1132
+ qs.stringify({ a: new Date(0) }, { filter: function (prefix, value) { return value instanceof Date ? 'kept' : value; } }),
1133
+ 'a=kept',
1134
+ 'a filter that transforms a Date wins over default serialization'
1135
+ );
1136
+ st.end();
1137
+ });
1138
+
1139
+ t.test('applies default Date handling to whatever a filter function returns', function (st) {
1140
+ var date = new Date(Date.UTC(2023, 2, 7, 9, 2, 41, 260));
1141
+ var iso = '2023-03-07T09%3A02%3A41.260Z';
1142
+ var identity = function (prefix, value) { return value; };
1143
+
1144
+ st.equal(
1145
+ qs.stringify({ a: { b: date } }, { filter: identity }),
1146
+ 'a%5Bb%5D=' + iso,
1147
+ 'a nested Date is serialized through a filter'
1148
+ );
1149
+ st.equal(
1150
+ qs.stringify(
1151
+ { a: date },
1152
+ { filter: identity, serializeDate: function (d) { return String(d.getTime()); } }
1153
+ ),
1154
+ 'a=1678179761260',
1155
+ '`serializeDate` is honored after a filter'
1156
+ );
1157
+ st.equal(
1158
+ qs.stringify({ a: [date, 'x'] }, { filter: identity, arrayFormat: 'comma' }),
1159
+ 'a=' + iso + '%2Cx',
1160
+ 'Dates inside a comma array are serialized through a filter, not stringified as locale text'
1161
+ );
1162
+ st.equal(
1163
+ qs.stringify({ a: 'x' }, { filter: function (prefix, value) { return prefix === 'a' ? date : value; } }),
1164
+ 'a=' + iso,
1165
+ 'a Date produced by the filter is serialized'
1166
+ );
1167
+ st.equal(
1168
+ qs.stringify({ a: date, b: 1 }, { filter: ['a'] }),
1169
+ 'a=' + iso,
1170
+ 'an array filter still serializes Dates'
1171
+ );
1172
+ st['throws'](
1173
+ function () { qs.stringify({ a: new Date(NaN) }, { filter: identity }); },
1174
+ RangeError,
1175
+ 'an invalid Date through a filter throws like the no-filter path'
1176
+ );
1177
+ st.end();
1178
+ });
1179
+
636
1180
  t.test('can disable uri encoding', function (st) {
637
1181
  st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
638
1182
  st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
@@ -687,13 +1231,28 @@ test('stringify()', function (t) {
687
1231
  st.end();
688
1232
  });
689
1233
 
1234
+ t.test('receives the default encoder as a second argument', function (st) {
1235
+ st.plan(8);
1236
+
1237
+ qs.stringify({ a: 1, b: new Date(), c: true, d: [1] }, {
1238
+ encoder: function (str) {
1239
+ st.match(typeof str, /^(?:string|number|boolean)$/);
1240
+ return '';
1241
+ }
1242
+ });
1243
+
1244
+ st.end();
1245
+ });
1246
+
690
1247
  t.test('receives the default encoder as a second argument', function (st) {
691
1248
  st.plan(2);
1249
+
692
1250
  qs.stringify({ a: 1 }, {
693
1251
  encoder: function (str, defaultEncoder) {
694
1252
  st.equal(defaultEncoder, utils.encode);
695
1253
  }
696
1254
  });
1255
+
697
1256
  st.end();
698
1257
  });
699
1258
 
@@ -821,16 +1380,53 @@ test('stringify()', function (t) {
821
1380
  st.equal(
822
1381
  qs.stringify(
823
1382
  { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
824
- { encodeValuesOnly: true }
1383
+ { encodeValuesOnly: true, arrayFormat: 'indices' }
825
1384
  ),
826
- 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'
1385
+ 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h',
1386
+ 'encodeValuesOnly + indices'
827
1387
  );
828
1388
  st.equal(
829
1389
  qs.stringify(
830
- { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }
1390
+ { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
1391
+ { encodeValuesOnly: true, arrayFormat: 'brackets' }
831
1392
  ),
832
- 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h'
1393
+ 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h',
1394
+ 'encodeValuesOnly + brackets'
833
1395
  );
1396
+ st.equal(
1397
+ qs.stringify(
1398
+ { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
1399
+ { encodeValuesOnly: true, arrayFormat: 'repeat' }
1400
+ ),
1401
+ 'a=b&c=d&c=e%3Df&f=g&f=h',
1402
+ 'encodeValuesOnly + repeat'
1403
+ );
1404
+
1405
+ st.equal(
1406
+ qs.stringify(
1407
+ { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
1408
+ { arrayFormat: 'indices' }
1409
+ ),
1410
+ 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h',
1411
+ 'no encodeValuesOnly + indices'
1412
+ );
1413
+ st.equal(
1414
+ qs.stringify(
1415
+ { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
1416
+ { arrayFormat: 'brackets' }
1417
+ ),
1418
+ 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h',
1419
+ 'no encodeValuesOnly + brackets'
1420
+ );
1421
+ st.equal(
1422
+ qs.stringify(
1423
+ { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
1424
+ { arrayFormat: 'repeat' }
1425
+ ),
1426
+ 'a=b&c=d&c=e&f=g&f=h',
1427
+ 'no encodeValuesOnly + repeat'
1428
+ );
1429
+
834
1430
  st.end();
835
1431
  });
836
1432
 
@@ -845,6 +1441,28 @@ test('stringify()', function (t) {
845
1441
  st.end();
846
1442
  });
847
1443
 
1444
+ t.test('strictNullHandling: applies the formatter to the encoded key (RFC1738)', function (st) {
1445
+ st.equal(
1446
+ qs.stringify(
1447
+ { 'a b': null, 'c d': 'e f' },
1448
+ { strictNullHandling: false, format: 'RFC1738' }
1449
+ ),
1450
+ 'a+b=&c+d=e+f',
1451
+ 'without: as expected'
1452
+ );
1453
+
1454
+ st.equal(
1455
+ qs.stringify(
1456
+ { 'a b': null, 'c d': 'e f' },
1457
+ { strictNullHandling: true, format: 'RFC1738' }
1458
+ ),
1459
+ 'a+b&c+d=e+f',
1460
+ 'with: as expected'
1461
+ );
1462
+
1463
+ st.end();
1464
+ });
1465
+
848
1466
  t.test('throws if an invalid charset is specified', function (st) {
849
1467
  st['throws'](function () {
850
1468
  qs.stringify({ a: 'b' }, { charset: 'foobar' });
@@ -867,13 +1485,25 @@ test('stringify()', function (t) {
867
1485
  st.end();
868
1486
  });
869
1487
 
870
- t.test('adds the right sentinel when instructed to and the charset is utf-8', function (st) {
871
- st.equal(qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), 'utf8=%E2%9C%93&a=%C3%A6');
872
- st.end();
873
- });
1488
+ t.test('`charsetSentinel` option', function (st) {
1489
+ st.equal(
1490
+ qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }),
1491
+ 'utf8=%E2%9C%93&a=%C3%A6',
1492
+ 'adds the right sentinel when instructed to and the charset is utf-8'
1493
+ );
1494
+
1495
+ st.equal(
1496
+ qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }),
1497
+ 'utf8=%26%2310003%3B&a=%E6',
1498
+ 'adds the right sentinel when instructed to and the charset is iso-8859-1'
1499
+ );
1500
+
1501
+ st.equal(
1502
+ qs.stringify({ a: 1, b: 2 }, { charsetSentinel: true, delimiter: ';' }),
1503
+ 'utf8=%E2%9C%93;a=1;b=2',
1504
+ 'uses the configured delimiter after the sentinel'
1505
+ );
874
1506
 
875
- t.test('adds the right sentinel when instructed to and the charset is iso-8859-1', function (st) {
876
- st.equal(qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), 'utf8=%26%2310003%3B&a=%E6');
877
1507
  st.end();
878
1508
  });
879
1509
 
@@ -916,6 +1546,15 @@ test('stringify()', function (t) {
916
1546
  };
917
1547
 
918
1548
  st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
1549
+
1550
+ var noopEncoder = function () { return 'x'; };
1551
+ noopEncoder();
1552
+ st['throws'](
1553
+ function () { encoder('x', noopEncoder, 'utf-8', 'unknown'); },
1554
+ 'this should never happen! type: unknown',
1555
+ 'encoder throws for unexpected type'
1556
+ );
1557
+
919
1558
  st.end();
920
1559
  });
921
1560
 
@@ -924,13 +1563,15 @@ test('stringify()', function (t) {
924
1563
  var withArray = { a: { b: [{ c: 'd', e: 'f' }] } };
925
1564
 
926
1565
  st.equal(qs.stringify(obj, { encode: false }), 'a[b][c]=d&a[b][e]=f', 'no array, no arrayFormat');
927
- st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'bracket' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
1566
+ st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'brackets' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
928
1567
  st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'indices' }), 'a[b][c]=d&a[b][e]=f', 'no array, indices');
1568
+ st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'no array, repeat');
929
1569
  st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'comma' }), 'a[b][c]=d&a[b][e]=f', 'no array, comma');
930
1570
 
931
1571
  st.equal(qs.stringify(withArray, { encode: false }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, no arrayFormat');
932
- st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'bracket' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, bracket');
1572
+ st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'brackets' }), 'a[b][][c]=d&a[b][][e]=f', 'array, bracket');
933
1573
  st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'indices' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, indices');
1574
+ st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'array, repeat');
934
1575
  st.equal(
935
1576
  qs.stringify(withArray, { encode: false, arrayFormat: 'comma' }),
936
1577
  '???',
@@ -943,10 +1584,42 @@ test('stringify()', function (t) {
943
1584
 
944
1585
  t.test('stringifies sparse arrays', function (st) {
945
1586
  /* eslint no-sparse-arrays: 0 */
946
- st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true }), 'a[1]=2&a[4]=1');
947
- st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true }), 'a[1][b][2][c]=1');
948
- st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true }), 'a[1][2][3][c]=1');
949
- st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true }), 'a[1][2][3][c][1]=1');
1587
+ st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1]=2&a[4]=1');
1588
+ st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=2&a[]=1');
1589
+ st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=2&a=1');
1590
+
1591
+ st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][b][2][c]=1');
1592
+ st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][b][][c]=1');
1593
+ st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[b][c]=1');
1594
+
1595
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c]=1');
1596
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c]=1');
1597
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
1598
+
1599
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c][1]=1');
1600
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c][]=1');
1601
+ st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
1602
+
1603
+ st.end();
1604
+ });
1605
+
1606
+ t.test('encodes a very long string', function (st) {
1607
+ var chars = [];
1608
+ var expected = [];
1609
+ for (var i = 0; i < 5e3; i++) {
1610
+ chars.push(' ' + i);
1611
+
1612
+ expected.push('%20' + i);
1613
+ }
1614
+
1615
+ var obj = {
1616
+ foo: chars.join('')
1617
+ };
1618
+
1619
+ st.equal(
1620
+ qs.stringify(obj, { arrayFormat: 'brackets', charset: 'utf-8' }),
1621
+ 'foo=' + expected.join('')
1622
+ );
950
1623
 
951
1624
  st.end();
952
1625
  });
@@ -957,7 +1630,21 @@ test('stringify()', function (t) {
957
1630
  test('stringifies empty keys', function (t) {
958
1631
  emptyTestCases.forEach(function (testCase) {
959
1632
  t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
960
- st.deepEqual(qs.stringify(testCase.withEmptyKeys, { encode: false }), testCase.stringifyOutput);
1633
+ st.deepEqual(
1634
+ qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }),
1635
+ testCase.stringifyOutput.indices,
1636
+ 'test case: ' + testCase.input + ', indices'
1637
+ );
1638
+ st.deepEqual(
1639
+ qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }),
1640
+ testCase.stringifyOutput.brackets,
1641
+ 'test case: ' + testCase.input + ', brackets'
1642
+ );
1643
+ st.deepEqual(
1644
+ qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }),
1645
+ testCase.stringifyOutput.repeat,
1646
+ 'test case: ' + testCase.input + ', repeat'
1647
+ );
961
1648
 
962
1649
  st.end();
963
1650
  });
@@ -966,6 +1653,112 @@ test('stringifies empty keys', function (t) {
966
1653
  t.test('edge case with object/arrays', function (st) {
967
1654
  st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3');
968
1655
  st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
1656
+ st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3');
1657
+ st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3&[a]=2');
1658
+
1659
+ st.end();
1660
+ });
1661
+
1662
+ t.test('stringifies non-string keys', function (st) {
1663
+ var S = Object('abc');
1664
+ S.toString = function () {
1665
+ return 'd';
1666
+ };
1667
+ var actual = qs.stringify({ a: 'b', 'false': {}, 1e+22: 'c', d: 'e' }, {
1668
+ filter: ['a', false, null, 10000000000000000000000, S],
1669
+ allowDots: true,
1670
+ encodeDotInKeys: true
1671
+ });
1672
+
1673
+ st.equal(actual, 'a=b&1e%2B22=c&d=e', 'stringifies correctly');
1674
+
1675
+ st.end();
1676
+ });
1677
+
1678
+ t.test('round-trips keys containing percent-encoded bracket text', function (st) {
1679
+ var cases = [
1680
+ { 'a%5Bb': 'c' },
1681
+ { 'a%5Db': 'c' },
1682
+ { 'a%255Bb': 'c' },
1683
+ { 'a%255Db': 'c' },
1684
+ { a: { 'b%5Bc': 'd' } },
1685
+ { a: { 'b%255Bc': 'd' } },
1686
+ { 'a%5B%255Bb': 'c' }
1687
+ ];
1688
+ for (var i = 0; i < cases.length; i++) {
1689
+ st.deepEqual(
1690
+ qs.parse(qs.stringify(cases[i])),
1691
+ cases[i],
1692
+ 'round-trips ' + JSON.stringify(cases[i])
1693
+ );
1694
+ }
1695
+
1696
+ st.end();
1697
+ });
1698
+
1699
+ t.test('parses input containing percent-encoded bracket text without mangling', function (st) {
1700
+ st.deepEqual(qs.parse('a%25255Bb=c'), { 'a%255Bb': 'c' }, 'a%25255Bb decodes to a%255Bb, not a%5Bb');
1701
+ st.deepEqual(qs.parse('a%25255Db=c'), { 'a%255Db': 'c' }, 'a%25255Db decodes to a%255Db, not a%5Db');
1702
+ st.deepEqual(qs.parse('a%5Bb%25255Bc%5D=d'), { a: { 'b%255Bc': 'd' } }, 'nested %25255B decodes to %255B inside segment, not %5B');
1703
+
1704
+ st.end();
1705
+ });
1706
+
1707
+ t.test('limits nesting depth with the `depth` option', function (st) {
1708
+ st.equal(
1709
+ qs.stringify({ a: { b: { c: 'd' } } }),
1710
+ 'a%5Bb%5D%5Bc%5D=d',
1711
+ 'the default depth is unlimited and does not alter output'
1712
+ );
1713
+ st.equal(
1714
+ qs.stringify({ a: { b: { c: 'd' } } }, { depth: 2 }),
1715
+ 'a%5Bb%5D%5Bc%5D=d',
1716
+ 'does not throw when nesting is within the depth limit'
1717
+ );
1718
+ st.equal(
1719
+ qs.stringify({ a: { b: 'c' } }, { depth: Infinity }),
1720
+ 'a%5Bb%5D=c',
1721
+ 'an explicit Infinity depth is unlimited'
1722
+ );
1723
+ st.equal(
1724
+ qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: null }),
1725
+ qs.stringify({ a: { b: { c: { d: 'e' } } } }),
1726
+ 'a non-number depth is ignored and treated as unlimited'
1727
+ );
1728
+ st.equal(
1729
+ qs.stringify({ a: 'b' }, { depth: 0 }),
1730
+ 'a=b',
1731
+ 'a flat object serializes with a depth of 0'
1732
+ );
1733
+
1734
+ var deep = {};
1735
+ var cursor = deep;
1736
+ for (var i = 0; i < 250; i++) {
1737
+ cursor.a = {};
1738
+ cursor = cursor.a;
1739
+ }
1740
+ cursor.b = 'c';
1741
+ st.equal(typeof qs.stringify(deep), 'string', 'the default depth serializes deeply nested objects without throwing');
1742
+
1743
+ st['throws'](
1744
+ function () { qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 }); },
1745
+ RangeError,
1746
+ 'throws a RangeError when nesting exceeds the depth limit'
1747
+ );
1748
+ st['throws'](
1749
+ function () { qs.stringify({ a: { b: 'c' } }, { depth: 0 }); },
1750
+ RangeError,
1751
+ 'a depth of 0 throws for any nested object'
1752
+ );
1753
+
1754
+ var caught;
1755
+ try {
1756
+ qs.stringify({ a: { b: { c: { d: 'e' } } } }, { depth: 2 });
1757
+ } catch (e) {
1758
+ caught = e;
1759
+ }
1760
+ st.ok(caught instanceof RangeError, 'the thrown value is a RangeError');
1761
+ st.equal(caught.message, 'Input depth exceeded depth option of 2', 'the RangeError names the exceeded depth');
969
1762
 
970
1763
  st.end();
971
1764
  });