@suveren/gateway 0.29.0 → 0.30.0
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.
- package/content/integrations/gmail.json +13 -2
- package/content/integrations/mail.json +12 -2
- package/content/integrations/mollie.json +10 -0
- package/dist/control-plane/index.mjs +125 -37
- package/dist/mcp-server/http.mjs +772 -420
- package/dist/ui/assets/index-C2BnlNib.js +123 -0
- package/dist/ui/assets/{index-DUIlMVlP.css → index-DPgewsLG.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
- package/node_modules/qs/CHANGELOG.md +38 -14
- package/node_modules/qs/README.md +31 -3
- package/node_modules/qs/dist/qs.js +20 -20
- package/node_modules/qs/lib/parse.js +94 -4
- package/node_modules/qs/lib/stringify.js +4 -4
- package/node_modules/qs/lib/utils.js +130 -6
- package/node_modules/qs/package.json +4 -4
- package/node_modules/qs/test/parse.js +174 -0
- package/node_modules/qs/test/stringify.js +263 -33
- package/node_modules/qs/test/utils.js +184 -0
- package/node_modules/sanitize-html/index.js +47 -10
- package/node_modules/sanitize-html/package.json +2 -3
- package/package.json +1 -1
- package/dist/ui/assets/index-C3vTF2Ye.js +0 -123
- package/node_modules/deepmerge/.editorconfig +0 -7
- package/node_modules/deepmerge/.eslintcache +0 -1
- package/node_modules/deepmerge/changelog.md +0 -167
- package/node_modules/deepmerge/dist/cjs.js +0 -133
- package/node_modules/deepmerge/dist/umd.js +0 -139
- package/node_modules/deepmerge/index.d.ts +0 -20
- package/node_modules/deepmerge/index.js +0 -106
- package/node_modules/deepmerge/license.txt +0 -21
- package/node_modules/deepmerge/package.json +0 -42
- package/node_modules/deepmerge/readme.md +0 -264
- package/node_modules/deepmerge/rollup.config.js +0 -22
|
@@ -62,10 +62,39 @@ test('parse()', function (t) {
|
|
|
62
62
|
st.end();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
t.test('comma: `[]=` in a value does not wrap the value in an extra array', function (st) {
|
|
66
|
+
st.deepEqual(qs.parse('a[b]=1,2,3[]=x', { comma: true }), { a: { b: ['1', '2', '3[]=x'] } });
|
|
67
|
+
st.deepEqual(qs.parse('a[b]=1,2[]=3', { comma: true }), { a: { b: ['1', '2[]=3'] } });
|
|
68
|
+
st.deepEqual(qs.parse('a[]=1,2,3', { comma: true }), { a: [['1', '2', '3']] });
|
|
69
|
+
st.end();
|
|
70
|
+
});
|
|
71
|
+
|
|
65
72
|
t.test('allows enabling dot notation', function (st) {
|
|
66
73
|
st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' });
|
|
67
74
|
st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } });
|
|
68
75
|
|
|
76
|
+
// a literal dot inside a bracket segment must not be rewritten by allowDots (#617)
|
|
77
|
+
st.deepEqual(
|
|
78
|
+
qs.parse('outer[a.b]=1', { allowDots: true }),
|
|
79
|
+
{ outer: { 'a.b': '1' } },
|
|
80
|
+
'a dot inside brackets stays part of the key when allowDots is true'
|
|
81
|
+
);
|
|
82
|
+
st.deepEqual(
|
|
83
|
+
qs.parse('outer[a.b]=1', { allowDots: true }),
|
|
84
|
+
qs.parse('outer[a.b]=1', { allowDots: false }),
|
|
85
|
+
'allowDots does not change how a bracket segment containing a dot is parsed'
|
|
86
|
+
);
|
|
87
|
+
st.deepEqual(
|
|
88
|
+
qs.parse('a].b=c', { allowDots: true }),
|
|
89
|
+
{ 'a]': { b: 'c' } },
|
|
90
|
+
'a stray close bracket does not stop a later dot from being converted'
|
|
91
|
+
);
|
|
92
|
+
st.deepEqual(
|
|
93
|
+
qs.parse('a]].b[c.d]=e', { allowDots: true }),
|
|
94
|
+
{ 'a]]': { b: { 'c.d': 'e' } } },
|
|
95
|
+
'stray close brackets do not unbalance a later bracket segment'
|
|
96
|
+
);
|
|
97
|
+
|
|
69
98
|
st.end();
|
|
70
99
|
});
|
|
71
100
|
|
|
@@ -360,6 +389,14 @@ test('parse()', function (t) {
|
|
|
360
389
|
st.end();
|
|
361
390
|
});
|
|
362
391
|
|
|
392
|
+
t.test('currently moves the key/value split to a later `]=` when the first `=` comes before any bracket group', function (st) {
|
|
393
|
+
characterizeParse(st, 'a=b[c]=d', undefined, { 'a=b': { c: 'd' } }, 'literal brackets after the first `=`');
|
|
394
|
+
characterizeParse(st, 'q=%5B1%5D=2', undefined, { 'q=': ['2'] }, 'encoded brackets collapse to an array');
|
|
395
|
+
characterizeParse(st, 'role=guest%5Bx%5Dy%5D=z', undefined, { 'role=guest': { x: 'z' } }, 'group opens after the first `=`, and the text after it is dropped');
|
|
396
|
+
characterizeParse(st, 'role=guest%5D=admin', undefined, { 'role=guest]': 'admin' }, 'encoded `]=` with no bracket group at all');
|
|
397
|
+
st.end();
|
|
398
|
+
});
|
|
399
|
+
|
|
363
400
|
t.test('allows to specify array indices', function (st) {
|
|
364
401
|
st.deepEqual(qs.parse('a[1]=c&a[0]=b&a[2]=d'), { a: ['b', 'c', 'd'] });
|
|
365
402
|
st.deepEqual(qs.parse('a[1]=c&a[0]=b'), { a: ['b', 'c'] });
|
|
@@ -745,6 +782,38 @@ test('parse()', function (t) {
|
|
|
745
782
|
st.end();
|
|
746
783
|
});
|
|
747
784
|
|
|
785
|
+
t.test('does not mutate an object input', function (st) {
|
|
786
|
+
var arr = ['x'];
|
|
787
|
+
st.deepEqual(qs.parse({ a: arr, 'a[]': 'y' }), { a: ['x', 'y'] }, 'appends to a copy of an input array');
|
|
788
|
+
st.deepEqual(arr, ['x'], 'the input array is unchanged');
|
|
789
|
+
|
|
790
|
+
var nested = { b: ['x'] };
|
|
791
|
+
st.deepEqual(qs.parse({ a: nested, 'a[b][]': 'y' }), { a: { b: ['x', 'y'] } }, 'appends to a copy of a nested input array');
|
|
792
|
+
st.deepEqual(nested, { b: ['x'] }, 'the nested input array is unchanged');
|
|
793
|
+
|
|
794
|
+
var obj = { b: 'c' };
|
|
795
|
+
st.deepEqual(qs.parse({ a: obj, 'a[d]': 'e' }), { a: { b: 'c', d: 'e' } }, 'merges into a copy of an input object');
|
|
796
|
+
st.deepEqual(obj, { b: 'c' }, 'the input object is unchanged');
|
|
797
|
+
|
|
798
|
+
var sparse = { b: [1, undefined, 2] };
|
|
799
|
+
st.deepEqual(qs.parse({ a: sparse }), { a: { b: [1, 2] } }, 'compacts a copy of an input array');
|
|
800
|
+
st.deepEqual(sparse, { b: [1, undefined, 2] }, 'the input array is not compacted');
|
|
801
|
+
|
|
802
|
+
var nullProto = { __proto__: null, b: 'c' };
|
|
803
|
+
st.equal(Object.getPrototypeOf(qs.parse({ a: nullProto }).a), null, 'a null-prototype input object stays null-prototype');
|
|
804
|
+
|
|
805
|
+
var date = new Date(0);
|
|
806
|
+
st.equal(qs.parse({ a: date }).a, date, 'a non-plain object is not copied');
|
|
807
|
+
|
|
808
|
+
var cyclic = { b: 'c' };
|
|
809
|
+
cyclic.self = cyclic;
|
|
810
|
+
var result = qs.parse({ a: cyclic });
|
|
811
|
+
st.notEqual(result.a, cyclic, 'a cyclic input object is copied');
|
|
812
|
+
st.equal(result.a.self, result.a, 'the copy keeps the cycle');
|
|
813
|
+
|
|
814
|
+
st.end();
|
|
815
|
+
});
|
|
816
|
+
|
|
748
817
|
t.test('does not blow up when Buffer global is missing', function (st) {
|
|
749
818
|
var restore = mockProperty(global, 'Buffer', { 'delete': true });
|
|
750
819
|
|
|
@@ -962,6 +1031,38 @@ test('parse()', function (t) {
|
|
|
962
1031
|
st.end();
|
|
963
1032
|
});
|
|
964
1033
|
|
|
1034
|
+
t.test('strictMerge conflicts do not overwrite indexed values already at that path', function (st) {
|
|
1035
|
+
st.deepEqual(
|
|
1036
|
+
qs.parse('a[z]=o&a=p&a[y]=q&a[2]=keep&[a]=r'),
|
|
1037
|
+
{ a: { 0: { z: 'o' }, 1: 'p', 2: 'keep', 3: 'r', y: 'q' } },
|
|
1038
|
+
'an indexed key merged into the container is kept'
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
st.deepEqual(
|
|
1042
|
+
qs.parse('a[z]=o&a=p&a[2]=keep&a[y]=q&[a]=r'),
|
|
1043
|
+
{ a: { 0: { z: 'o' }, 1: 'p', 2: 'keep', 3: 'r', y: 'q' } },
|
|
1044
|
+
'an indexed key that extended the container while it was an array is kept'
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
st.end();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
t.test('strictMerge conflicts do not overwrite values once the container overflows', function (st) {
|
|
1051
|
+
st.deepEqual(
|
|
1052
|
+
qs.parse('a[z]=v0&a=v1&a[y]=v2&[a]=v3&[a]x=v4&a[2]=v5&[a]x1=v6', { arrayLimit: 2 }),
|
|
1053
|
+
{ a: { 0: { z: 'v0' }, 1: 'v1', 2: ['v3', 'v5'], 3: 'v4', 4: 'v6', y: 'v2' } },
|
|
1054
|
+
'a conflict appended after the container overflowed keeps every value'
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
st.deepEqual(
|
|
1058
|
+
qs.parse('a[2]=v1&a=v5&a=v7&[a]x=v9', { parseArrays: false, arrayLimit: 1 }),
|
|
1059
|
+
{ a: { 0: 'v5', 1: 'v7', 2: 'v1', 3: 'v9' } },
|
|
1060
|
+
'an index the container already holds is not reused'
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
st.end();
|
|
1064
|
+
});
|
|
1065
|
+
|
|
965
1066
|
t.test('dunder proto is ignored', function (st) {
|
|
966
1067
|
var payload = 'categories[__proto__]=login&categories[__proto__]&categories[length]=42';
|
|
967
1068
|
var result = qs.parse(payload, { allowPrototypes: true });
|
|
@@ -1463,6 +1564,57 @@ test('parse()', function (t) {
|
|
|
1463
1564
|
sst.end();
|
|
1464
1565
|
});
|
|
1465
1566
|
|
|
1567
|
+
st.test('enforces arrayLimit when a strictMerge conflict appends to its container', function (sst) {
|
|
1568
|
+
var aliased = 'a[z]=o&a=p&a[y]=q&[a]=r';
|
|
1569
|
+
sst['throws'](
|
|
1570
|
+
function () { qs.parse(aliased, { arrayLimit: 2, throwOnLimitExceeded: true }); },
|
|
1571
|
+
new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
|
|
1572
|
+
'a third conflicting value throws once the container would exceed arrayLimit'
|
|
1573
|
+
);
|
|
1574
|
+
sst.deepEqual(
|
|
1575
|
+
qs.parse(aliased, { arrayLimit: 3, throwOnLimitExceeded: true }),
|
|
1576
|
+
{ a: { 0: { z: 'o' }, 1: 'p', 2: 'r', y: 'q' } },
|
|
1577
|
+
'appends while the container stays within arrayLimit'
|
|
1578
|
+
);
|
|
1579
|
+
sst.deepEqual(
|
|
1580
|
+
qs.parse(aliased, { arrayLimit: 2 }),
|
|
1581
|
+
{ a: { 0: { z: 'o' }, 1: 'p', 2: 'r', y: 'q' } },
|
|
1582
|
+
'appends without throwing when throwOnLimitExceeded is not set'
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
sst.end();
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
st.test('enforces arrayLimit when differently spelled keys collide', function (sst) {
|
|
1589
|
+
sst['throws'](
|
|
1590
|
+
function () { qs.parse('[a]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1591
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1592
|
+
'two primitives throw, as `a=x&a=y` does'
|
|
1593
|
+
);
|
|
1594
|
+
sst['throws'](
|
|
1595
|
+
function () { qs.parse('a[b]=x&a=y', { arrayLimit: 1, throwOnLimitExceeded: true }); },
|
|
1596
|
+
new RangeError('Array limit exceeded. Only 1 element allowed in an array.'),
|
|
1597
|
+
'an object and a primitive throw under strictMerge'
|
|
1598
|
+
);
|
|
1599
|
+
sst.deepEqual(
|
|
1600
|
+
qs.parse('[a]=x&a=y', { arrayLimit: 1 }),
|
|
1601
|
+
{ a: { 0: 'x', 1: 'y' } },
|
|
1602
|
+
'without throwOnLimitExceeded, two primitives convert to an object, as `a=x&a=y` does'
|
|
1603
|
+
);
|
|
1604
|
+
sst.deepEqual(
|
|
1605
|
+
qs.parse('a[b]=x&a=y', { arrayLimit: 1 }),
|
|
1606
|
+
{ a: { 0: { b: 'x' }, 1: 'y' } },
|
|
1607
|
+
'and so do an object and a primitive'
|
|
1608
|
+
);
|
|
1609
|
+
sst.deepEqual(
|
|
1610
|
+
qs.parse('a[b]=x&a=y', { arrayLimit: 2, throwOnLimitExceeded: true }),
|
|
1611
|
+
{ a: [{ b: 'x' }, 'y'] },
|
|
1612
|
+
'a collision within arrayLimit is still an array'
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
sst.end();
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1466
1618
|
st.test('does not throw when cumulative comma combine stays within arrayLimit', function (sst) {
|
|
1467
1619
|
var result = qs.parse('a=1,2,3&a=4', { comma: true, arrayLimit: 5, throwOnLimitExceeded: true });
|
|
1468
1620
|
sst.deepEqual(result, { a: ['1', '2', '3', '4'] }, 'combined array within limit is preserved');
|
|
@@ -1807,6 +1959,28 @@ test('`duplicates` option', function (t) {
|
|
|
1807
1959
|
st.end();
|
|
1808
1960
|
});
|
|
1809
1961
|
|
|
1962
|
+
t.test('`[]=` in a value does not force combining', function (st) {
|
|
1963
|
+
st.deepEqual(
|
|
1964
|
+
qs.parse('a[b]=1&a[b]=2[]=3', { duplicates: 'last' }),
|
|
1965
|
+
{ a: { b: '2[]=3' } },
|
|
1966
|
+
'duplicates last: `[]=` in the value is not a bracketed key'
|
|
1967
|
+
);
|
|
1968
|
+
|
|
1969
|
+
st.deepEqual(
|
|
1970
|
+
qs.parse('a[b]=1&a[b]=2[]=3', { duplicates: 'first' }),
|
|
1971
|
+
{ a: { b: '1' } },
|
|
1972
|
+
'duplicates first: `[]=` in the value is not a bracketed key'
|
|
1973
|
+
);
|
|
1974
|
+
|
|
1975
|
+
st.deepEqual(
|
|
1976
|
+
qs.parse('a[b]=1&a[b]=2[]=3', { duplicates: 'combine' }),
|
|
1977
|
+
{ a: { b: ['1', '2[]=3'] } },
|
|
1978
|
+
'duplicates combine: still combines'
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
st.end();
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1810
1984
|
t.end();
|
|
1811
1985
|
});
|
|
1812
1986
|
|
|
@@ -212,63 +212,94 @@ test('stringify()', function (t) {
|
|
|
212
212
|
st.end();
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
|
|
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) {
|
|
215
|
+
t.test('does not encode the structural separator dots between nested keys when encodeDotInKeys and allowDots are provided', function (st) {
|
|
222
216
|
st.equal(
|
|
223
|
-
qs.stringify(
|
|
224
|
-
|
|
225
|
-
|
|
217
|
+
qs.stringify(
|
|
218
|
+
{ a: { b: { c: 'd' } } },
|
|
219
|
+
{ allowDots: true, encodeDotInKeys: true }
|
|
220
|
+
),
|
|
221
|
+
'a.b.c=d',
|
|
222
|
+
'no key contains a literal dot, so encodeDotInKeys is a no-op on the separators'
|
|
226
223
|
);
|
|
227
224
|
st.equal(
|
|
228
|
-
qs.stringify(
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
qs.stringify(
|
|
226
|
+
{ a: { b: { c: { d: 'e' } } } },
|
|
227
|
+
{ allowDots: true, encodeDotInKeys: true }
|
|
228
|
+
),
|
|
229
|
+
'a.b.c.d=e',
|
|
230
|
+
'every nesting separator is preserved, not just the last one'
|
|
231
231
|
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
|
|
233
|
+
st.deepEqual(
|
|
234
|
+
qs.parse(
|
|
235
|
+
qs.stringify({ a: { b: { c: 'd' } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
236
|
+
{ allowDots: true, decodeDotInKeys: true }
|
|
237
|
+
),
|
|
238
|
+
{ a: { b: { c: 'd' } } },
|
|
239
|
+
'stringify -> parse round-trips a deeply nested object'
|
|
236
240
|
);
|
|
241
|
+
|
|
242
|
+
st.end();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
t.test('encodeDotInKeys encodes only literal dots inside nested keys, never the structural separators, at any depth', function (st) {
|
|
237
246
|
st.equal(
|
|
238
247
|
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
239
|
-
'a%
|
|
240
|
-
'nested
|
|
248
|
+
'a.b%252Ec.d=e',
|
|
249
|
+
'a nested key with a literal dot and an object value encodes only its own dot'
|
|
250
|
+
);
|
|
251
|
+
st.equal(
|
|
252
|
+
qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
253
|
+
'a%252Eb.c.d=e',
|
|
254
|
+
'a top-level dotted key stays encoded while deeper separators stay literal'
|
|
241
255
|
);
|
|
242
256
|
st.equal(
|
|
243
257
|
qs.stringify({ a: { b: { 'c.d': { e: 'f' } } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
244
|
-
'a%
|
|
245
|
-
'deeply nested
|
|
258
|
+
'a.b.c%252Ed.e=f',
|
|
259
|
+
'a literal dot in a deeply nested key is encoded without touching the separators around it'
|
|
246
260
|
);
|
|
247
261
|
|
|
248
262
|
st.equal(
|
|
249
263
|
qs.stringify({ a: { b: [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
|
|
250
|
-
'a%
|
|
251
|
-
'
|
|
264
|
+
'a.b%5B0%5D=1&a.b%5B1%5D=2',
|
|
265
|
+
'separator dots are preserved for an array value nested under keys'
|
|
252
266
|
);
|
|
253
267
|
st.equal(
|
|
254
268
|
qs.stringify({ a: { 'b.c': [1, 2] } }, { allowDots: true, encodeDotInKeys: true }),
|
|
255
|
-
'a%
|
|
256
|
-
'
|
|
269
|
+
'a.b%252Ec%5B0%5D=1&a.b%252Ec%5B1%5D=2',
|
|
270
|
+
'a nested dotted key with an array value encodes only its own dot'
|
|
257
271
|
);
|
|
258
272
|
|
|
259
|
-
st.
|
|
260
|
-
qs.
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
st.deepEqual(
|
|
274
|
+
qs.parse(
|
|
275
|
+
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
276
|
+
{ allowDots: true, decodeDotInKeys: true }
|
|
277
|
+
),
|
|
278
|
+
{ a: { 'b.c': { d: 'e' } } },
|
|
279
|
+
'stringify -> parse round-trips a nested dotted key with an object value'
|
|
263
280
|
);
|
|
264
|
-
|
|
265
281
|
st.deepEqual(
|
|
266
282
|
qs.parse(
|
|
267
|
-
qs.stringify({ a: {
|
|
283
|
+
qs.stringify({ 'a.b': { c: { d: 'e' } } }, { allowDots: true, encodeDotInKeys: true }),
|
|
268
284
|
{ allowDots: true, decodeDotInKeys: true }
|
|
269
285
|
),
|
|
270
|
-
{ 'a.b': { c:
|
|
271
|
-
'
|
|
286
|
+
{ 'a.b': { c: { d: 'e' } } },
|
|
287
|
+
'stringify -> parse round-trips a top-level dotted key with deep nesting'
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
st.end();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
t.test('with allowDots false, encodeDotInKeys leaves nested-key dots literal, consistently for object and primitive values', function (st) {
|
|
294
|
+
st.equal(
|
|
295
|
+
qs.stringify({ a: { 'b.c': { d: 'e' } } }, { allowDots: false, encodeDotInKeys: true }),
|
|
296
|
+
'a%5Bb.c%5D%5Bd%5D=e',
|
|
297
|
+
'nested dotted key with an object value'
|
|
298
|
+
);
|
|
299
|
+
st.equal(
|
|
300
|
+
qs.stringify({ a: { 'b.c': 'e' } }, { allowDots: false, encodeDotInKeys: true }),
|
|
301
|
+
'a%5Bb.c%5D=e',
|
|
302
|
+
'nested dotted key with a primitive value matches the object-value case'
|
|
272
303
|
);
|
|
273
304
|
|
|
274
305
|
st.end();
|
|
@@ -394,6 +425,41 @@ test('stringify()', function (t) {
|
|
|
394
425
|
st.end();
|
|
395
426
|
});
|
|
396
427
|
|
|
428
|
+
t.test('ignores inherited array format names', function (st) {
|
|
429
|
+
var array = { a: ['b', 'c'] };
|
|
430
|
+
var indices = 'a%5B0%5D=b&a%5B1%5D=c';
|
|
431
|
+
var repeat = 'a=b&a=c';
|
|
432
|
+
|
|
433
|
+
st.equal(qs.stringify(array, { arrayFormat: 'unsupported' }), indices, 'unknown format falls back to `indices`');
|
|
434
|
+
|
|
435
|
+
var names = [
|
|
436
|
+
'constructor',
|
|
437
|
+
'toString',
|
|
438
|
+
'__proto__',
|
|
439
|
+
'valueOf',
|
|
440
|
+
'hasOwnProperty',
|
|
441
|
+
'isPrototypeOf',
|
|
442
|
+
'propertyIsEnumerable',
|
|
443
|
+
'toLocaleString'
|
|
444
|
+
];
|
|
445
|
+
for (var i = 0; i < names.length; i++) {
|
|
446
|
+
var name = names[i];
|
|
447
|
+
st.equal(qs.stringify(array, { arrayFormat: name }), indices, '`' + name + '` falls back to `indices`');
|
|
448
|
+
st.equal(
|
|
449
|
+
qs.stringify(array, { arrayFormat: name, indices: true }),
|
|
450
|
+
indices,
|
|
451
|
+
'`' + name + '` with `indices: true` falls back to `indices`'
|
|
452
|
+
);
|
|
453
|
+
st.equal(
|
|
454
|
+
qs.stringify(array, { arrayFormat: name, indices: false }),
|
|
455
|
+
repeat,
|
|
456
|
+
'`' + name + '` with `indices: false` falls back to `repeat`'
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
st.end();
|
|
461
|
+
});
|
|
462
|
+
|
|
397
463
|
t.test('`skipNulls` option', function (st) {
|
|
398
464
|
st.equal(
|
|
399
465
|
qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
|
|
@@ -569,6 +635,53 @@ test('stringify()', function (t) {
|
|
|
569
635
|
st.end();
|
|
570
636
|
});
|
|
571
637
|
|
|
638
|
+
t.test('encodes comma array values with the same encoder arguments as other values, with encodeValuesOnly', function (st) {
|
|
639
|
+
var commaValuesOnly = { arrayFormat: 'comma', encodeValuesOnly: true };
|
|
640
|
+
|
|
641
|
+
st.equal(
|
|
642
|
+
qs.stringify({ a: ['é', 'x'], b: 'é' }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)),
|
|
643
|
+
'a=%E9,x&b=%E9',
|
|
644
|
+
'respects charset iso-8859-1'
|
|
645
|
+
);
|
|
646
|
+
st.deepEqual(
|
|
647
|
+
qs.parse(qs.stringify({ a: ['é', 'x'] }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)), { charset: 'iso-8859-1', comma: true }),
|
|
648
|
+
{ a: ['é', 'x'] },
|
|
649
|
+
'round-trips in iso-8859-1'
|
|
650
|
+
);
|
|
651
|
+
st.equal(
|
|
652
|
+
qs.stringify({ a: ['☺'] }, utils.assign({ charset: 'iso-8859-1' }, commaValuesOnly)),
|
|
653
|
+
'a=%26%239786%3B',
|
|
654
|
+
'encodes unrepresentable chars as numeric entities in iso-8859-1'
|
|
655
|
+
);
|
|
656
|
+
st.equal(
|
|
657
|
+
qs.stringify({ a: ['(x)', 'y z'], b: '(x)' }, utils.assign({ format: qs.formats.RFC1738 }, commaValuesOnly)),
|
|
658
|
+
'a=(x),y+z&b=(x)',
|
|
659
|
+
'respects format RFC1738'
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
var calls = [];
|
|
663
|
+
st.equal(
|
|
664
|
+
qs.stringify({ a: ['b', 'c'] }, utils.assign({
|
|
665
|
+
encoder: function (str, defaultEncoder, charset, kind, format) {
|
|
666
|
+
calls[calls.length] = [str, typeof defaultEncoder, charset, kind, format];
|
|
667
|
+
return defaultEncoder(str, defaultEncoder, charset, kind, format).toUpperCase();
|
|
668
|
+
}
|
|
669
|
+
}, commaValuesOnly)),
|
|
670
|
+
'a=B,C',
|
|
671
|
+
'a custom encoder can call defaultEncoder'
|
|
672
|
+
);
|
|
673
|
+
st.deepEqual(
|
|
674
|
+
calls,
|
|
675
|
+
[
|
|
676
|
+
['b', 'function', 'utf-8', 'value', qs.formats.RFC3986],
|
|
677
|
+
['c', 'function', 'utf-8', 'value', qs.formats.RFC3986]
|
|
678
|
+
],
|
|
679
|
+
'a custom encoder receives defaultEncoder, charset, kind, and format'
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
st.end();
|
|
683
|
+
});
|
|
684
|
+
|
|
572
685
|
t.test('stringifies comma and empty non-array values', function (st) {
|
|
573
686
|
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), 'a=,&b=&c=c,d%');
|
|
574
687
|
st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), 'a=,&b=&c=c,d%');
|
|
@@ -865,6 +978,112 @@ test('stringify()', function (t) {
|
|
|
865
978
|
st.end();
|
|
866
979
|
});
|
|
867
980
|
|
|
981
|
+
t.test('arrayFormat=comma: does not crash on entries with no callable `toString`', function (st) {
|
|
982
|
+
var json = JSON.parse('{ "a": ["b", { "toString": "x" }, "c"] }');
|
|
983
|
+
st.equal(qs.stringify(json, { arrayFormat: 'comma' }), 'a=b%2C%5Bobject%20Object%5D%2Cc', 'JSON own `toString`');
|
|
984
|
+
st.equal(qs.stringify(json, { arrayFormat: 'comma', encode: false }), 'a=b,[object Object],c', 'with encode: false');
|
|
985
|
+
st.equal(
|
|
986
|
+
qs.stringify(json, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
987
|
+
'a=b,%5Bobject%20Object%5D,c',
|
|
988
|
+
'with encodeValuesOnly'
|
|
989
|
+
);
|
|
990
|
+
st.equal(
|
|
991
|
+
qs.stringify(JSON.parse('{ "a": [{ "toString": "x" }] }'), { arrayFormat: 'comma', commaRoundTrip: true }),
|
|
992
|
+
'a%5B%5D=%5Bobject%20Object%5D',
|
|
993
|
+
'with commaRoundTrip'
|
|
994
|
+
);
|
|
995
|
+
st.equal(
|
|
996
|
+
qs.stringify(JSON.parse('{ "a": [{ "toString": "x" }] }'), { arrayFormat: 'comma' }),
|
|
997
|
+
qs.stringify({ a: [{ b: 1 }] }, { arrayFormat: 'comma' }),
|
|
998
|
+
'renders the same as a plain object entry'
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
st.equal(
|
|
1002
|
+
qs.stringify(qs.parse('a[][b]=c', { plainObjects: true }), { arrayFormat: 'comma' }),
|
|
1003
|
+
'a=%5Bobject%20Object%5D',
|
|
1004
|
+
'null-prototype entry from `parse` with plainObjects'
|
|
1005
|
+
);
|
|
1006
|
+
st.equal(
|
|
1007
|
+
qs.stringify(qs.parse('a[][b]=c', { plainObjects: true }), { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
1008
|
+
'a=%5Bobject%20Object%5D',
|
|
1009
|
+
'null-prototype entry from `parse` with plainObjects, with encodeValuesOnly'
|
|
1010
|
+
);
|
|
1011
|
+
st.equal(
|
|
1012
|
+
qs.stringify(qs.parse('a[0][toString]=x', { allowPrototypes: true }), { arrayFormat: 'comma' }),
|
|
1013
|
+
'a=%5Bobject%20Object%5D',
|
|
1014
|
+
'own `toString` entry from `parse` with allowPrototypes'
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
var nested = JSON.parse('{ "a": [[1, [{ "toString": "x" }]], "y"] }');
|
|
1018
|
+
st.equal(qs.stringify(nested, { arrayFormat: 'comma', encode: false }), 'a=1,[object Object],y', 'nested array');
|
|
1019
|
+
st.equal(
|
|
1020
|
+
qs.stringify(nested, { arrayFormat: 'comma', encodeValuesOnly: true }),
|
|
1021
|
+
'a=1%2C%5Bobject%20Object%5D,y',
|
|
1022
|
+
'nested array, with encodeValuesOnly'
|
|
1023
|
+
);
|
|
1024
|
+
st.equal(qs.stringify({ a: [[1, 2], 3] }, { arrayFormat: 'comma', encode: false }), 'a=1,2,3', 'nested array is still flattened');
|
|
1025
|
+
|
|
1026
|
+
var cyclic = [{ toString: 'x' }];
|
|
1027
|
+
cyclic[cyclic.length] = cyclic;
|
|
1028
|
+
st.equal(
|
|
1029
|
+
qs.stringify({ a: cyclic }, { arrayFormat: 'comma', encode: false }),
|
|
1030
|
+
'a=[object Object],[object Object],',
|
|
1031
|
+
'cyclic nested array joins its cycle as empty'
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
var customJoin = [{ toString: 'x' }];
|
|
1035
|
+
customJoin.join = function () { return 'j'; };
|
|
1036
|
+
st.equal(qs.stringify({ a: [customJoin, 'y'] }, { arrayFormat: 'comma', encode: false }), 'a=j,y', 'own `join` is still used');
|
|
1037
|
+
|
|
1038
|
+
st.equal(
|
|
1039
|
+
qs.stringify({ a: [{ toString: 'x', valueOf: function () { return 7; } }] }, { arrayFormat: 'comma' }),
|
|
1040
|
+
'a=7',
|
|
1041
|
+
'primitive `valueOf` is still used'
|
|
1042
|
+
);
|
|
1043
|
+
st.equal(
|
|
1044
|
+
qs.stringify({ a: [{ toString: 'x', valueOf: function () { return null; } }] }, { arrayFormat: 'comma' }),
|
|
1045
|
+
'a=null',
|
|
1046
|
+
'null `valueOf` is still used'
|
|
1047
|
+
);
|
|
1048
|
+
st.equal(
|
|
1049
|
+
qs.stringify({ a: [{ toString: 'x', valueOf: function () { return {}; } }] }, { arrayFormat: 'comma' }),
|
|
1050
|
+
'a=%5Bobject%20Object%5D',
|
|
1051
|
+
'non-primitive `valueOf` falls back'
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
st.equal(
|
|
1055
|
+
qs.stringify({ a: [{ toString: function () { return 't'; } }, new Date(0)] }, { arrayFormat: 'comma', encode: false }),
|
|
1056
|
+
'a=t,1970-01-01T00:00:00.000Z',
|
|
1057
|
+
'callable `toString` and dates are unaffected'
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
st['throws'](
|
|
1061
|
+
function () {
|
|
1062
|
+
qs.stringify({ a: [{ toString: function () { throw new SyntaxError('mine'); } }] }, { arrayFormat: 'comma' });
|
|
1063
|
+
},
|
|
1064
|
+
SyntaxError,
|
|
1065
|
+
'a throwing `toString` still throws'
|
|
1066
|
+
);
|
|
1067
|
+
st['throws'](
|
|
1068
|
+
function () {
|
|
1069
|
+
qs.stringify({ a: [{ toString: 'x', valueOf: function () { throw new EvalError('mine'); } }] }, { arrayFormat: 'comma' });
|
|
1070
|
+
},
|
|
1071
|
+
EvalError,
|
|
1072
|
+
'a throwing `valueOf` still throws'
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
st.end();
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
t.test('arrayFormat=comma: entries with `Symbol.toPrimitive` are unaffected', { skip: !hasSymbols() || typeof Symbol.toPrimitive !== 'symbol' }, function (st) {
|
|
1079
|
+
var entry = { toString: 'x' };
|
|
1080
|
+
entry[Symbol.toPrimitive] = function () { return 'p'; };
|
|
1081
|
+
st.equal(qs.stringify({ a: [entry] }, { arrayFormat: 'comma' }), 'a=p');
|
|
1082
|
+
st.equal(qs.stringify({ a: [entry] }, { arrayFormat: 'comma', encodeValuesOnly: true }), 'a=p');
|
|
1083
|
+
|
|
1084
|
+
st.end();
|
|
1085
|
+
});
|
|
1086
|
+
|
|
868
1087
|
t.test('stringifies a null object', { skip: !hasProto }, function (st) {
|
|
869
1088
|
st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
|
|
870
1089
|
st.end();
|
|
@@ -1480,6 +1699,17 @@ test('stringify()', function (t) {
|
|
|
1480
1699
|
st.end();
|
|
1481
1700
|
});
|
|
1482
1701
|
|
|
1702
|
+
t.test('encodes a literal `+` as `%2B` in iso-8859-1 mode so it round-trips', function (st) {
|
|
1703
|
+
st.equal(qs.stringify({ a: 'b+c' }, { charset: 'iso-8859-1' }), 'a=b%2Bc', 'a `+` in a value is percent-encoded');
|
|
1704
|
+
st.equal(qs.stringify({ 'a+b': 'c' }, { charset: 'iso-8859-1' }), 'a%2Bb=c', 'a `+` in a key is percent-encoded');
|
|
1705
|
+
st.deepEqual(
|
|
1706
|
+
qs.parse(qs.stringify({ a: 'b+c' }, { charset: 'iso-8859-1' }), { charset: 'iso-8859-1' }),
|
|
1707
|
+
{ a: 'b+c' },
|
|
1708
|
+
'the `+` survives a round-trip instead of decoding to a space'
|
|
1709
|
+
);
|
|
1710
|
+
st.end();
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1483
1713
|
t.test('respects an explicit charset of utf-8 (the default)', function (st) {
|
|
1484
1714
|
st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
|
|
1485
1715
|
st.end();
|