@sap/cds-dk 10.2.0 → 10.2.1

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 (90) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/bind.js +5 -4
  3. package/bin/completion/scripts/cds.fish +4 -3
  4. package/bin/completion/scripts/cds.ps1 +4 -4
  5. package/bin/completion/scripts/cds.sh +4 -4
  6. package/bin/completion/scripts/cds.zsh +4 -4
  7. package/bin/debug.js +3 -3
  8. package/bin/deploy/index.js +2 -2
  9. package/bin/deploy/to-hana/hana.js +1 -1
  10. package/bin/deploy/to-hana/hdiDeployUtil.js +25 -14
  11. package/bin/up.js +3 -3
  12. package/bin/version.js +1 -1
  13. package/bin/watch/vite-watch.js +12 -6
  14. package/lib/bind/shared.js +1 -1
  15. package/lib/init/index.js +1 -0
  16. package/lib/init/projectReader.js +3 -2
  17. package/lib/init/template/approuter/files/values.yaml.hbs +0 -9
  18. package/lib/init/template/approuter/files/xs-app.json.hbs +0 -22
  19. package/lib/init/template/approuter/index.js +8 -9
  20. package/lib/init/template/html5-repo/index.js +12 -0
  21. package/lib/init/template/react/files/vite.config.{{extension}}.hbs +2 -3
  22. package/lib/init/template/react/index.js +1 -3
  23. package/lib/init/template/sample/files/app/index.html.hbs +18 -0
  24. package/lib/init/template/sample/index.js +1 -0
  25. package/lib/init/template/vue/files/vite.config.{{extension}}.hbs +2 -3
  26. package/lib/init/template/vue/index.js +1 -3
  27. package/lib/mtx/push.js +7 -1
  28. package/node_modules/@cap-js/asyncapi/README.md +4 -14
  29. package/node_modules/@cap-js/asyncapi/cds-plugin.js +12 -0
  30. package/node_modules/@cap-js/asyncapi/index.js +3 -2
  31. package/node_modules/@cap-js/asyncapi/lib/compile/components/messages/index.js +4 -2
  32. package/node_modules/@cap-js/asyncapi/lib/compile/components/schemas/definition.js +11 -1
  33. package/node_modules/@cap-js/asyncapi/lib/compile/components/schemas/utils.js +16 -0
  34. package/node_modules/@cap-js/asyncapi/lib/compile/utils.js +4 -3
  35. package/node_modules/@cap-js/asyncapi/lib/import/importAsyncAPI.js +34 -0
  36. package/node_modules/@cap-js/asyncapi/lib/import/index.js +18 -0
  37. package/node_modules/@cap-js/asyncapi/lib/import/message.js +9 -0
  38. package/node_modules/@cap-js/asyncapi/lib/import/parserContext.js +368 -0
  39. package/node_modules/@cap-js/asyncapi/lib/import/utils.js +85 -0
  40. package/node_modules/@cap-js/asyncapi/package.json +89 -1
  41. package/node_modules/@sap/cds/CHANGELOG.md +10 -0
  42. package/node_modules/@sap/cds/app/index.js +1 -1
  43. package/node_modules/@sap/cds/lib/env/schemas/cds-package.js +0 -2
  44. package/node_modules/@sap/cds/lib/env/schemas/cds-rc.js +3 -3
  45. package/node_modules/@sap/cds/lib/env/schemas/index.js +4 -4
  46. package/node_modules/@sap/cds/lib/utils/cds-utils.js +21 -8
  47. package/node_modules/@sap/cds/libx/_runtime/messaging/common-utils/connections.js +45 -39
  48. package/node_modules/@sap/cds/libx/odata/ODataAdapter.js +1 -1
  49. package/node_modules/@sap/cds/libx/queue/TaskRunner.js +22 -5
  50. package/node_modules/@sap/cds/package.json +1 -1
  51. package/node_modules/@sap/cds-mtxs/CHANGELOG.md +8 -0
  52. package/node_modules/@sap/cds-mtxs/package.json +1 -1
  53. package/node_modules/@sap/cds-mtxs/srv/extensibility-service.js +57 -41
  54. package/node_modules/@sap/cds-mtxs/srv/jobs-service.js +27 -12
  55. package/node_modules/@sap/cds-mtxs/srv/plugins/hana.js +31 -25
  56. package/node_modules/body-parser/README.md +17 -13
  57. package/node_modules/body-parser/lib/read.js +74 -57
  58. package/node_modules/body-parser/lib/types/json.js +11 -11
  59. package/node_modules/body-parser/lib/types/urlencoded.js +5 -5
  60. package/node_modules/body-parser/lib/utils.js +0 -15
  61. package/node_modules/body-parser/package.json +7 -12
  62. package/node_modules/encodeurl/README.md +14 -0
  63. package/node_modules/encodeurl/index.js +5 -10
  64. package/node_modules/encodeurl/package.json +10 -6
  65. package/node_modules/on-finished/index.js +14 -2
  66. package/node_modules/on-finished/package.json +11 -7
  67. package/node_modules/qs/.editorconfig +1 -1
  68. package/node_modules/qs/CHANGELOG.md +87 -22
  69. package/node_modules/qs/README.md +63 -10
  70. package/node_modules/qs/dist/qs.js +17 -17
  71. package/node_modules/qs/eslint.config.mjs +61 -50
  72. package/node_modules/qs/lib/formats.js +7 -2
  73. package/node_modules/qs/lib/parse.js +29 -10
  74. package/node_modules/qs/lib/stringify.js +5 -3
  75. package/node_modules/qs/lib/utils.js +28 -9
  76. package/node_modules/qs/package.json +6 -6
  77. package/node_modules/qs/test/parse.js +335 -2
  78. package/node_modules/qs/test/stringify.js +222 -0
  79. package/node_modules/qs/test/utils.js +61 -3
  80. package/node_modules/semver/classes/range.js +2 -2
  81. package/node_modules/semver/internal/constants.js +30 -0
  82. package/node_modules/semver/internal/trim-comparator-whitespace.js +438 -0
  83. package/node_modules/semver/package.json +1 -1
  84. package/node_modules/ws/README.md +21 -29
  85. package/node_modules/ws/lib/buffer-util.js +13 -2
  86. package/node_modules/ws/lib/sender.js +4 -1
  87. package/node_modules/ws/lib/websocket.js +8 -3
  88. package/node_modules/ws/package.json +1 -1
  89. package/package.json +1 -1
  90. package/node_modules/on-finished/HISTORY.md +0 -98
@@ -2,7 +2,7 @@
2
2
  "name": "qs",
3
3
  "description": "A querystring parser that supports nesting and arrays, with a depth limit",
4
4
  "homepage": "https://github.com/ljharb/qs",
5
- "version": "6.16.1",
5
+ "version": "6.17.1",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/ljharb/qs.git"
@@ -43,8 +43,8 @@
43
43
  "common-shakeify": "~1.0.0",
44
44
  "eclint": "^2.8.1",
45
45
  "es-value-fixtures": "^1.7.1",
46
- "eslint": "^10.12.0",
47
- "evalmd": "^0.0.22",
46
+ "eslint": "^10.13.0",
47
+ "evalmd": "^0.1.0",
48
48
  "for-each": "^0.3.5",
49
49
  "glob": "=10.3.7",
50
50
  "has-bigints": "^1.1.0",
@@ -59,7 +59,7 @@
59
59
  "mkdirp": "^0.5.5",
60
60
  "mock-property": "^1.1.2",
61
61
  "module-deps": "^6.2.3",
62
- "npmignore": "^0.3.5",
62
+ "npmignore": "^0.3.6",
63
63
  "nyc": "^10.3.2",
64
64
  "object-inspect": "^1.13.4",
65
65
  "qs-iconv": "^1.0.4",
@@ -72,12 +72,12 @@
72
72
  "prepack": "npmignore --auto --commentLines=autogenerated && npm run dist",
73
73
  "prepublishOnly": "safe-publish-latest",
74
74
  "prepublish": "not-in-publish || npm run prepublishOnly",
75
- "pretest": "npm run --silent readme && npm run --silent lint",
75
+ "pretest": "npm run lint",
76
76
  "test": "npm run tests-only",
77
77
  "tests-only": "nyc tape 'test/**/*.js'",
78
78
  "posttest": "npx npm@'>=10.2' audit --production",
79
- "readme": "evalmd README.md",
80
79
  "postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)",
80
+ "prelint": "evalmd --eslint README.md",
81
81
  "lint": "eslint .",
82
82
  "dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js"
83
83
  },
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var test = require('tape');
4
+ var forEach = require('for-each');
4
5
  var hasPropertyDescriptors = require('has-property-descriptors')();
5
6
  var iconv = require('iconv-lite');
6
7
  var mockProperty = require('mock-property');
@@ -98,6 +99,21 @@ test('parse()', function (t) {
98
99
  st.end();
99
100
  });
100
101
 
102
+ t.test('decode dot keys with case-insensitive percent escapes', function (st) {
103
+ var input = 'name%252eobj.first%252ename=John&name%252Eobj.last%252Ename=Doe';
104
+ st.deepEqual(
105
+ qs.parse(input, { decodeDotInKeys: true }),
106
+ { 'name.obj': { 'first.name': 'John', 'last.name': 'Doe' } },
107
+ 'lowercase and uppercase escapes decode in both parent and child keys'
108
+ );
109
+ st.deepEqual(
110
+ qs.parse(input, { allowDots: true, decodeDotInKeys: false }),
111
+ { 'name%2eobj': { 'first%2ename': 'John' }, 'name%2Eobj': { 'last%2Ename': 'Doe' } },
112
+ 'escaped dots remain unchanged when decoding is disabled'
113
+ );
114
+ st.end();
115
+ });
116
+
101
117
  t.test('decode dot keys correctly', function (st) {
102
118
  st.deepEqual(
103
119
  qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { allowDots: false, decodeDotInKeys: false }),
@@ -258,8 +274,34 @@ test('parse()', function (t) {
258
274
  st.end();
259
275
  });
260
276
 
277
+ t.test('ignores bracket-wrapped prototype keys when depth = 0 and allowPrototypes is false', function (st) {
278
+ st.deepEqual(qs.parse('[toString]=foo', { depth: 0 }), {}, 'the stored key is the bracket content');
279
+ st.deepEqual(qs.parse('[hasOwnProperty]=bar', { depth: 0 }), {});
280
+ st.deepEqual(qs.parse('%5BhasOwnProperty%5D=bar', { depth: 0 }), {}, 'percent-encoded brackets');
281
+ st.deepEqual(qs.parse('[toString]=foo&a=b', { depth: 0 }), { a: 'b' });
282
+ st.deepEqual(qs.parse('[toString]=foo', { depth: false }), {}, 'depth: false');
283
+ st.deepEqual(qs.parse('.toString=foo', { depth: 0, allowDots: true }), {}, 'a leading dot becomes a bracket group');
284
+ st.deepEqual(qs.parse('[a]=b&[toString]=foo', { depth: 0 }), { a: 'b' }, 'other bracket-wrapped keys still strip their brackets');
285
+ st.end();
286
+ });
287
+
261
288
  t.test('allows prototype keys when depth = 0 and allowPrototypes is true', function (st) {
262
289
  st.deepEqual(qs.parse('toString=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
290
+ st.deepEqual(qs.parse('[toString]=foo', { depth: 0, allowPrototypes: true }), { toString: 'foo' });
291
+ st.end();
292
+ });
293
+
294
+ t.test('keeps bracket-wrapped prototype keys when depth = 0 and plainObjects is true', function (st) {
295
+ var result = qs.parse('[toString]=foo', { depth: 0, plainObjects: true });
296
+ st.deepEqual(result, { __proto__: null, toString: 'foo' });
297
+ st.equal(Object.getPrototypeOf(result), null);
298
+ st.end();
299
+ });
300
+
301
+ t.test('strips one wrapping bracket pair from the key when depth = 0', function (st) {
302
+ st.deepEqual(qs.parse('[a]=b', { depth: 0 }), { a: 'b' });
303
+ st.deepEqual(qs.parse('[[toString]]=foo', { depth: 0 }), { '[toString]': 'foo' }, 'only the outer pair is stripped');
304
+ st.deepEqual(qs.parse('x[toString]=foo', { depth: 0 }), { 'x[toString]': 'foo' }, 'a key with a parent stays literal');
263
305
  st.end();
264
306
  });
265
307
 
@@ -1139,6 +1181,67 @@ test('parse()', function (t) {
1139
1181
  st.end();
1140
1182
  });
1141
1183
 
1184
+ var withOwnProto = function (value) {
1185
+ var obj = {};
1186
+ Object.defineProperty(obj, '__proto__', {
1187
+ configurable: true,
1188
+ enumerable: true,
1189
+ value: value,
1190
+ writable: true
1191
+ });
1192
+ return obj;
1193
+ };
1194
+ // node < 0.12 reads an own `__proto__` data property as `Object.prototype`, so it can not be copied by value
1195
+ var ownProtoIsReadable = withOwnProto(1).__proto__ === 1; // eslint-disable-line no-proto
1196
+
1197
+ t.test('object-valued input with only an own `__proto__` does not mutate sub-object [[Prototype]]', function (st) {
1198
+ // `JSON.parse` in node < 0.12 sets the [[Prototype]] instead of creating an own `__proto__`
1199
+ var out = qs.parse({ user: withOwnProto({ isAdmin: true }) });
1200
+
1201
+ st.equal(out.user.isAdmin, undefined, 'attacker-controlled inherited property is not exposed');
1202
+ st.equal(Object.getPrototypeOf(out.user), Object.prototype, 'sub-object [[Prototype]] is unchanged');
1203
+ st.equal(Object.prototype.isAdmin, undefined, 'Object.prototype is not polluted');
1204
+
1205
+ var sibling = withOwnProto({ isAdmin: true });
1206
+ sibling.name = 'alice';
1207
+ var withSibling = qs.parse({ user: sibling });
1208
+
1209
+ st.equal(withSibling.user.name, 'alice', 'sibling key is preserved');
1210
+ st.equal(withSibling.user.isAdmin, undefined, 'a sibling key does not hide an inherited property');
1211
+ st.equal(Object.getPrototypeOf(withSibling.user), Object.prototype, 'a sibling key does not change [[Prototype]]');
1212
+
1213
+ st.end();
1214
+ });
1215
+
1216
+ t.test('object-valued input keeps an own `__proto__` as a data property', { skip: !ownProtoIsReadable }, function (st) {
1217
+ var descriptor = function (value) {
1218
+ return {
1219
+ configurable: true,
1220
+ enumerable: true,
1221
+ value: value,
1222
+ writable: true
1223
+ };
1224
+ };
1225
+
1226
+ st.deepEqual(
1227
+ Object.getOwnPropertyDescriptor(qs.parse({ user: withOwnProto({ isAdmin: true }) }).user, '__proto__'),
1228
+ descriptor({ isAdmin: true }),
1229
+ 'an object value is stored as a data property'
1230
+ );
1231
+ st.deepEqual(
1232
+ Object.getOwnPropertyDescriptor(qs.parse({ user: withOwnProto('admin') }).user, '__proto__'),
1233
+ descriptor('admin'),
1234
+ 'a primitive value is stored as a data property'
1235
+ );
1236
+ st.deepEqual(
1237
+ Object.getOwnPropertyDescriptor(qs.parse({ 'user[name]': 'alice', user: withOwnProto({ isAdmin: true }) }).user, '__proto__'),
1238
+ descriptor({ isAdmin: true }),
1239
+ 'it survives a merge with a bracket key'
1240
+ );
1241
+
1242
+ st.end();
1243
+ });
1244
+
1142
1245
  t.test('can return null objects', { skip: !hasProto }, function (st) {
1143
1246
  var expected = {
1144
1247
  __proto__: null,
@@ -1162,6 +1265,19 @@ test('parse()', function (t) {
1162
1265
  st.end();
1163
1266
  });
1164
1267
 
1268
+ t.test('returns null objects for empty brackets when parseArrays is false', { skip: !hasProto }, function (st) {
1269
+ st.deepEqual(
1270
+ qs.parse('a[]=b', { parseArrays: false, plainObjects: true }),
1271
+ { __proto__: null, a: { __proto__: null, 0: 'b' } }
1272
+ );
1273
+ st.deepEqual(
1274
+ qs.parse('a[]=b&a[hasOwnProperty]=c', { parseArrays: false, plainObjects: true }),
1275
+ { __proto__: null, a: { __proto__: null, 0: 'b', hasOwnProperty: 'c' } },
1276
+ 'prototype keys land on a null object'
1277
+ );
1278
+ st.end();
1279
+ });
1280
+
1165
1281
  t.test('can parse with custom encoding', function (st) {
1166
1282
  st.deepEqual(qs.parse('%8c%a7=%91%e5%8d%e3%95%7b', {
1167
1283
  decoder: function (str) {
@@ -1254,6 +1370,31 @@ test('parse()', function (t) {
1254
1370
  st.end();
1255
1371
  });
1256
1372
 
1373
+ t.test('interprets numeric entities above U+FFFF as surrogate pairs', function (st) {
1374
+ var options = { charset: 'iso-8859-1', interpretNumericEntities: true };
1375
+ st.deepEqual(qs.parse('foo=%26%23128512%3B', options), { foo: '\uD83D\uDE00' }, 'U+1F600, which a browser submits as `&#128512;` from an iso-8859-1 form');
1376
+ st.deepEqual(qs.parse('foo=%26%2365536%3B', options), { foo: '\uD800\uDC00' }, 'U+10000');
1377
+ st.deepEqual(qs.parse('foo=%26%231114111%3B', options), { foo: '\uDBFF\uDFFF' }, 'U+10FFFF');
1378
+ st.deepEqual(qs.parse('foo=%26%2365535%3B', options), { foo: '\uFFFF' }, 'U+FFFF is still one code unit');
1379
+ st.end();
1380
+ });
1381
+
1382
+ t.test('interprets numeric entities in each value, but not in keys', function (st) {
1383
+ var options = { charset: 'iso-8859-1', interpretNumericEntities: true };
1384
+ st.deepEqual(qs.parse('a=' + urlEncodedNumSmiley + ',x', options), { a: '☺,x' }, 'a value with commas stays one string without `comma`');
1385
+ st.deepEqual(qs.parse('a' + urlEncodedNumSmiley + '=x', options), { 'a&#9786;': 'x' }, 'keys are not interpreted');
1386
+ st.deepEqual(qs.parse('a[]=' + urlEncodedNumSmiley + '&a[]=x', options), { a: ['☺', 'x'] }, 'bracket array values');
1387
+ st.deepEqual(qs.parse('a=' + urlEncodedNumSmiley + '&a=x', options), { a: ['☺', 'x'] }, 'repeated key values');
1388
+ st.deepEqual(qs.parse('a[b]=' + urlEncodedNumSmiley, options), { a: { b: '☺' } }, 'nested key values');
1389
+ st.deepEqual(
1390
+ qs.parse('utf8=%26%2310003%3B&a=' + urlEncodedNumSmiley, { charsetSentinel: true, interpretNumericEntities: true }),
1391
+ { a: '☺' },
1392
+ 'an iso-8859-1 charset sentinel enables interpretation'
1393
+ );
1394
+ st.deepEqual(qs.parse('a=' + urlEncodedNumSmiley + ',x', { charset: 'iso-8859-1', comma: true }), { a: ['&#9786;', 'x'] }, '`comma` splits without `interpretNumericEntities`');
1395
+ st.end();
1396
+ });
1397
+
1257
1398
  t.test('handles a custom decoder returning `null`, in the `iso-8859-1` charset, when `interpretNumericEntities`', function (st) {
1258
1399
  st.deepEqual(qs.parse('foo=&bar=' + urlEncodedNumSmiley, {
1259
1400
  charset: 'iso-8859-1',
@@ -1289,15 +1430,70 @@ test('parse()', function (t) {
1289
1430
  st.end();
1290
1431
  });
1291
1432
 
1292
- t.test('interpretNumericEntities with comma:true and iso charset does not crash', function (st) {
1433
+ t.test('interpretNumericEntities preserves comma arrays in iso charset', function (st) {
1293
1434
  st.deepEqual(
1294
1435
  qs.parse('b&a[]=1,' + urlEncodedNumSmiley, { comma: true, charset: 'iso-8859-1', interpretNumericEntities: true }),
1295
- { b: '', a: ['1,☺'] }
1436
+ { b: '', a: [['1', '☺']] }
1296
1437
  );
1297
1438
 
1298
1439
  st.end();
1299
1440
  });
1300
1441
 
1442
+ t.test('interprets numeric entities per comma-separated value', function (st) {
1443
+ var options = { comma: true, charset: 'iso-8859-1', interpretNumericEntities: true };
1444
+ st.deepEqual(qs.parse('a=b,c', options), { a: ['b', 'c'] });
1445
+ st.deepEqual(qs.parse('a=' + urlEncodedNumSmiley + ',%26%239786%3B', options), { a: ['☺', '☺'] });
1446
+ st.deepEqual(qs.parse('a=%26%2344%3B,b%2Cc', options), { a: [',', 'b,c'] });
1447
+ st.deepEqual(qs.parse('a=,b,', options), { a: ['', 'b', ''] });
1448
+ st.deepEqual(qs.parse('a=b,c&a=d,e', options), { a: ['b', 'c', 'd', 'e'] });
1449
+ st.deepEqual(qs.parse('a[]=b,c&a[]=d,e', options), { a: [['b', 'c'], ['d', 'e']] });
1450
+ st.deepEqual(qs.parse('a[0]=b,c', options), { a: [['b', 'c']] });
1451
+ st.deepEqual(qs.parse('utf8=%26%2310003%3B&a=b,' + urlEncodedNumSmiley, {
1452
+ comma: true,
1453
+ charsetSentinel: true,
1454
+ interpretNumericEntities: true
1455
+ }), { a: ['b', '☺'] });
1456
+ st.deepEqual(qs.parse('a=b,c,d', {
1457
+ comma: true,
1458
+ charset: 'iso-8859-1',
1459
+ interpretNumericEntities: true,
1460
+ arrayLimit: 2
1461
+ }), { a: { 0: 'b', 1: 'c', 2: 'd' } });
1462
+ st['throws'](function () {
1463
+ qs.parse('a=b,c,d', {
1464
+ comma: true,
1465
+ charset: 'iso-8859-1',
1466
+ interpretNumericEntities: true,
1467
+ arrayLimit: 2,
1468
+ throwOnLimitExceeded: true
1469
+ });
1470
+ }, RangeError);
1471
+ st.end();
1472
+ });
1473
+
1474
+ t.test('preserves falsy decoder results inside comma arrays', function (st) {
1475
+ st.deepEqual(qs.parse('a=null,0,false,' + urlEncodedNumSmiley, {
1476
+ comma: true,
1477
+ charset: 'iso-8859-1',
1478
+ interpretNumericEntities: true,
1479
+ decoder: function (str, defaultDecoder, charset, type) {
1480
+ if (type === 'value') {
1481
+ if (str === 'null') {
1482
+ return null;
1483
+ }
1484
+ if (str === '0') {
1485
+ return 0;
1486
+ }
1487
+ if (str === 'false') {
1488
+ return false;
1489
+ }
1490
+ }
1491
+ return defaultDecoder(str, defaultDecoder, charset, type);
1492
+ }
1493
+ }), { a: [null, 0, false, '☺'] });
1494
+ st.end();
1495
+ });
1496
+
1301
1497
  t.test('does not interpret %uXXXX syntax in iso-8859-1 mode', function (st) {
1302
1498
  st.deepEqual(qs.parse('%u263A=%u263A', { charset: 'iso-8859-1' }), { '%u263A': '%u263A' });
1303
1499
  st.end();
@@ -1421,6 +1617,41 @@ test('parse()', function (t) {
1421
1617
  sst.end();
1422
1618
  });
1423
1619
 
1620
+ st.test('zero and negative arrayLimit allow non-array values', function (sst) {
1621
+ forEach([0, -1], function (arrayLimit) {
1622
+ var options = { arrayLimit: arrayLimit, throwOnLimitExceeded: true };
1623
+ var label = 'arrayLimit ' + arrayLimit;
1624
+ sst.deepEqual(qs.parse('a=b&c[d]=e', options), { a: 'b', c: { d: 'e' } }, label + ': query string fields');
1625
+ sst.deepEqual(qs.parse('a=b', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, comma: true }), { a: 'b' }, label + ': a value without commas when comma is enabled');
1626
+ sst.deepEqual(qs.parse({ a: 'b', 'c[d]': 'e' }, options), { a: 'b', c: { d: 'e' } }, label + ': object input fields');
1627
+ sst.deepEqual(qs.parse({ a: [] }, options), { a: [] }, label + ': an empty input array has no elements');
1628
+ sst.deepEqual(
1629
+ qs.parse('a=b&a=c', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, duplicates: 'last' }),
1630
+ { a: 'c' },
1631
+ label + ': replacing a duplicate does not create an array'
1632
+ );
1633
+ sst.deepEqual(
1634
+ qs.parse('a[]=', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, allowEmptyArrays: true }),
1635
+ { a: [] },
1636
+ label + ': an empty array has no elements to exceed the limit'
1637
+ );
1638
+ sst.deepEqual(qs.parse({ a: ['b'] }, options), { a: ['b'] }, label + ': an array value in object input is left as-is');
1639
+ sst.deepEqual(qs.parse({ 'a[b]': ['x'] }, options), { a: { b: ['x'] } }, label + ': an array value under a nested key in object input is left as-is');
1640
+ sst.deepEqual(qs.parse('a.b=c', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, allowDots: true }), { a: { b: 'c' } }, label + ': dotted keys');
1641
+ sst.deepEqual(
1642
+ qs.parse('a=b&a=c', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, duplicates: 'first' }),
1643
+ { a: 'b' },
1644
+ label + ': keeping the first duplicate does not create an array'
1645
+ );
1646
+ sst.deepEqual(
1647
+ qs.parse('a[]=b', { arrayLimit: arrayLimit, throwOnLimitExceeded: true, parseArrays: false }),
1648
+ { a: { 0: 'b' } },
1649
+ label + ': array syntax with parseArrays false does not create an array'
1650
+ );
1651
+ });
1652
+ sst.end();
1653
+ });
1654
+
1424
1655
  st.test('does not throw when at limit', function (sst) {
1425
1656
  // 3 elements = limit of 3, should not throw
1426
1657
  var result = qs.parse('a[]=1&a[]=2&a[]=3', { arrayLimit: 3, throwOnLimitExceeded: true });
@@ -1429,6 +1660,58 @@ test('parse()', function (t) {
1429
1660
  sst.end();
1430
1661
  });
1431
1662
 
1663
+ st.test('zero and negative arrayLimit', function (sst) {
1664
+ forEach([0, -1], function (arrayLimit) {
1665
+ var options = { arrayLimit: arrayLimit };
1666
+ var throwOptions = { arrayLimit: arrayLimit, throwOnLimitExceeded: true };
1667
+ var label = 'arrayLimit ' + arrayLimit;
1668
+
1669
+ sst.deepEqual(qs.parse('a=b&c[d]=e', options), { a: 'b', c: { d: 'e' } }, label + ': scalar and object fields are unaffected');
1670
+ sst.deepEqual(qs.parse('a[]=b', options), { a: { 0: 'b' } }, label + ': a bracket array becomes an object');
1671
+ sst.deepEqual(qs.parse('a[]=b&a[]=c', options), { a: { 0: 'b', 1: 'c' } }, label + ': a multi-element bracket array becomes an object');
1672
+ sst.deepEqual(qs.parse('a[0]=b', options), { a: { 0: 'b' } }, label + ': an indexed array becomes an object');
1673
+ sst.deepEqual(qs.parse('a=b&a=c', options), { a: { 0: 'b', 1: 'c' } }, label + ': combined duplicates become an object');
1674
+ sst.deepEqual(qs.parse('a[]=', options), { a: { 0: '' } }, label + ': an empty-string element still counts as an element');
1675
+ sst.deepEqual(qs.parse('a[]=', { arrayLimit: arrayLimit, allowEmptyArrays: true }), { a: [] }, label + ': an empty array has no elements');
1676
+ sst.deepEqual(qs.parse({ 'a[]': 'b' }, options), { a: { 0: 'b' } }, label + ': a bracket key in object input becomes an object');
1677
+ sst.deepEqual(qs.parse({ a: ['b'] }, options), { a: ['b'] }, label + ': an array value in object input is left as-is');
1678
+ sst.deepEqual(qs.parse({ a: [] }, options), { a: [] }, label + ': an empty array value in object input is left as-is');
1679
+
1680
+ var error = arrayLimit === 0
1681
+ ? new RangeError('Array limit exceeded. Only 0 elements allowed in an array.')
1682
+ : RangeError;
1683
+ sst['throws'](function () { qs.parse('a[]=b', throwOptions); }, error, label + ': throws on a bracket array');
1684
+ sst['throws'](function () { qs.parse('a[0]=b', throwOptions); }, error, label + ': throws on an indexed array');
1685
+ sst['throws'](function () { qs.parse('a=b&a=c', throwOptions); }, error, label + ': throws on combined duplicates');
1686
+ sst['throws'](function () { qs.parse('a[]=', throwOptions); }, error, label + ': throws on an empty-string element');
1687
+ sst['throws'](function () { qs.parse({ 'a[]': 'b' }, throwOptions); }, error, label + ': throws on a bracket key in object input');
1688
+ });
1689
+
1690
+ sst.end();
1691
+ });
1692
+
1693
+ st.test('zero and negative arrayLimit with comma', function (sst) {
1694
+ forEach([0, -1], function (arrayLimit) {
1695
+ var options = { arrayLimit: arrayLimit, comma: true };
1696
+ var throwOptions = { arrayLimit: arrayLimit, comma: true, throwOnLimitExceeded: true };
1697
+ var label = 'arrayLimit ' + arrayLimit;
1698
+
1699
+ sst.deepEqual(qs.parse('a=b', options), { a: 'b' }, label + ': a value without commas is unaffected');
1700
+ sst.deepEqual(qs.parse('a=b,c', options), { a: { 0: 'b', 1: 'c' } }, label + ': a comma value becomes an object');
1701
+ sst.deepEqual(qs.parse('a[]=b,c', options), { a: { 0: ['b', 'c'] } }, label + ': a bracket array holding a comma group becomes an object');
1702
+ sst.deepEqual(qs.parse('a[0]=b,c', options), { a: { 0: { 0: 'b', 1: 'c' } } }, label + ': an indexed comma group becomes an object');
1703
+
1704
+ var error = arrayLimit === 0
1705
+ ? new RangeError('Array limit exceeded. Only 0 elements allowed in an array.')
1706
+ : RangeError;
1707
+ sst['throws'](function () { qs.parse('a=b,c', throwOptions); }, error, label + ': throws on a comma value');
1708
+ sst['throws'](function () { qs.parse('a[]=b,c', throwOptions); }, error, label + ': throws on a comma group under `[]=`');
1709
+ sst['throws'](function () { qs.parse('a[0]=b,c', throwOptions); }, error, label + ': throws on an indexed comma group');
1710
+ });
1711
+
1712
+ sst.end();
1713
+ });
1714
+
1432
1715
  st.test('converts array to object if length is greater than limit', function (sst) {
1433
1716
  var result = qs.parse('a[1]=1&a[2]=2&a[3]=3&a[4]=4&a[5]=5&a[6]=6', { arrayLimit: 5 });
1434
1717
 
@@ -1564,6 +1847,27 @@ test('parse()', function (t) {
1564
1847
  sst.end();
1565
1848
  });
1566
1849
 
1850
+ st.test('counts the elements of a sparse indexed array on merge, not its holes', function (sst) {
1851
+ sst.deepEqual(qs.parse('a[]=new&a[19]=19&a[0]=0'), { a: ['new', '19', '0'] }, 'an array merged into a sparse array');
1852
+ sst.deepEqual(qs.parse('a[]=new&a[19]=19&a[0]=0', { throwOnLimitExceeded: true }), { a: ['new', '19', '0'] }, 'and it does not throw');
1853
+ sst.deepEqual(qs.parse('a[0]=w&a[19]=x&a[]=y'), { a: ['w', 'x', 'y'] }, 'a bracket value appended after a high index');
1854
+ sst.deepEqual(qs.parse('a[19]=x&a=y'), { a: ['x', 'y'] }, 'a scalar merged into a sparse array');
1855
+ sst.deepEqual(qs.parse('a[19]=x&a=y', { throwOnLimitExceeded: true }), { a: ['x', 'y'] }, 'and it does not throw');
1856
+
1857
+ sst.deepEqual(
1858
+ qs.parse('a[1]=x&a[]=y&a[]=z', { arrayLimit: 2 }),
1859
+ { a: { 0: 'y', 1: 'x', 2: 'z' } },
1860
+ 'more elements than arrayLimit still converts when the array has holes'
1861
+ );
1862
+ sst['throws'](
1863
+ function () { qs.parse('a[1]=x&a[]=y&a[]=z', { arrayLimit: 2, throwOnLimitExceeded: true }); },
1864
+ new RangeError('Array limit exceeded. Only 2 elements allowed in an array.'),
1865
+ 'and throws with throwOnLimitExceeded'
1866
+ );
1867
+
1868
+ sst.end();
1869
+ });
1870
+
1567
1871
  st.test('enforces arrayLimit when a strictMerge conflict appends to its container', function (sst) {
1568
1872
  var aliased = 'a[z]=o&a=p&a[y]=q&[a]=r';
1569
1873
  sst['throws'](
@@ -1621,6 +1925,35 @@ test('parse()', function (t) {
1621
1925
  sst.end();
1622
1926
  });
1623
1927
 
1928
+ st.test('throws when a comma array at arrayLimit is combined with one more value', function (sst) {
1929
+ var error = new RangeError('Array limit exceeded. Only 2 elements allowed in an array.');
1930
+ sst['throws'](
1931
+ function () { qs.parse('a=x,y&a=z', { comma: true, arrayLimit: 2, throwOnLimitExceeded: true }); },
1932
+ error,
1933
+ 'a plain value appended to a full comma array'
1934
+ );
1935
+ sst['throws'](
1936
+ function () { qs.parse('a=x&a=y,z', { comma: true, arrayLimit: 2, throwOnLimitExceeded: true }); },
1937
+ error,
1938
+ 'a comma value appended to a plain value'
1939
+ );
1940
+ sst.end();
1941
+ });
1942
+
1943
+ st.test('does not throw when a duplicate of a full comma array is discarded or replaces it', function (sst) {
1944
+ sst.deepEqual(
1945
+ qs.parse('a=x,y&a=z', { comma: true, arrayLimit: 2, throwOnLimitExceeded: true, duplicates: 'first' }),
1946
+ { a: ['x', 'y'] },
1947
+ 'duplicates: first keeps the full array'
1948
+ );
1949
+ sst.deepEqual(
1950
+ qs.parse('a=x,y&a=z', { comma: true, arrayLimit: 2, throwOnLimitExceeded: true, duplicates: 'last' }),
1951
+ { a: 'z' },
1952
+ 'duplicates: last replaces the full array'
1953
+ );
1954
+ sst.end();
1955
+ });
1956
+
1624
1957
  st.test('silently combines to an overflow object when throwOnLimitExceeded is not set', function (sst) {
1625
1958
  var result = qs.parse('a=1,2,3&a=4,5,6', { comma: true, arrayLimit: 5 });
1626
1959
  sst.deepEqual(result, { a: { 0: '1', 1: '2', 2: '3', 3: '4', 4: '5', 5: '6' } }, 'converts to object without throwing');