diff --git a/packages/host/package.json b/packages/host/package.json index 5e7af4e444..949e6f9e2f 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -68,7 +68,7 @@ "@types/matrix-js-sdk": "^11.0.1", "@types/ms": "^0.7.34", "@types/pluralize": "^0.0.30", - "@types/qs": "^6.9.14", + "@types/qs": "^6.9.17", "@types/qunit": "^2.11.3", "@types/rsvp": "^4.0.9", "@types/uuid": "^9.0.8", @@ -159,7 +159,7 @@ "pluralize": "^8.0.0", "prettier": "^3.0.3", "prettier-plugin-ember-template-tag": "^1.1.0", - "qs": "^6.12.3", + "qs": "^6.13.0", "qunit": "^2.20.0", "qunit-dom": "^2.0.0", "safe-stable-stringify": "^2.4.3", diff --git a/packages/host/tests/unit/qs-test.ts b/packages/host/tests/unit/qs-test.ts new file mode 100644 index 0000000000..61e689b9ea --- /dev/null +++ b/packages/host/tests/unit/qs-test.ts @@ -0,0 +1,54 @@ +import qs from 'qs'; +import { module, test } from 'qunit'; + +import { parseQuery, Query } from '@cardstack/runtime-common/query'; + +module('Unit | qs | parse', function () { + test('parseQuery errors out if the query is too deep', async function (assert) { + assert.throws( + () => parseQuery('a[b][c][d][e][f][g][h][i][j][k][l]=m'), + /RangeError: Input depth exceeded depth option of 10 and strictDepth is true/, + ); + }); + test('invertibility: applying stringify and parse on object will return the same object', async function (assert) { + let testRealmURL = 'https://example.com/'; + let query: Query = { + filter: { + on: { + module: `${testRealmURL}book`, + name: 'Book', + }, + every: [ + { + eq: { + 'author.firstName': 'Cardy', + }, + }, + { + any: [ + { + eq: { + 'author.lastName': 'Jones', + }, + }, + { + eq: { + 'author.lastName': 'Stackington Jr. III', + }, + }, + ], + }, + ], + }, + sort: [ + { + by: 'author.lastName', + on: { module: `${testRealmURL}book`, name: 'Book' }, + }, + ], + }; + let queryString = qs.stringify(query); + let parsedQuery: any = parseQuery(queryString); + assert.deepEqual(parsedQuery, query); + }); +}); diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index f8b8e304e0..e134386bfc 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -22,7 +22,7 @@ "@types/mime-types": "^2.1.1", "@types/node": "^18.18.5", "@types/pg": "^8.11.5", - "@types/qs": "^6.9.14", + "@types/qs": "^6.9.17", "@types/qunit": "^2.11.3", "@types/sane": "^2.0.1", "@types/supertest": "^2.0.12", @@ -51,7 +51,7 @@ "pg": "^8.11.5", "prettier": "^2.8.4", "prettier-plugin-ember-template-tag": "^1.1.0", - "qs": "^6.12.3", + "qs": "^6.13.0", "qunit": "^2.20.0", "sane": "^5.0.1", "sql-parser-cst": "^0.28.0", diff --git a/packages/runtime-common/package.json b/packages/runtime-common/package.json index 9398660397..cfed0fe384 100644 --- a/packages/runtime-common/package.json +++ b/packages/runtime-common/package.json @@ -25,7 +25,7 @@ "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.14.182", "@types/pluralize": "^0.0.30", - "@types/qs": "^6.9.14", + "@types/qs": "^6.9.17", "@types/uuid": "^9.0.8", "babel-import-util": "^1.2.2", "babel-plugin-ember-template-compilation": "^2.2.1", @@ -45,7 +45,7 @@ "loglevel": "^1.8.1", "marked": "^12.0.1", "pluralize": "^8.0.0", - "qs": "^6.12.3", + "qs": "^6.13.0", "qunit": "^2.20.0", "recast": "^0.23.4", "safe-stable-stringify": "^2.4.3", diff --git a/packages/runtime-common/query.ts b/packages/runtime-common/query.ts index 0dcc968081..4dc3501351 100644 --- a/packages/runtime-common/query.ts +++ b/packages/runtime-common/query.ts @@ -307,9 +307,9 @@ function assertEveryFilter( `${pointer.join('/') || '/'}: every must be an array of Filters`, ); } else { - filter.every.every((value: any, index: number) => - assertFilter(value, pointer.concat(`[${index}]`)), - ); + filter.every.forEach((value: any, index: number) => { + assertFilter(value, pointer.concat(`[${index}]`)); + }); } } @@ -348,9 +348,10 @@ function assertEqFilter( if (typeof filter.eq !== 'object' || filter.eq == null) { throw new Error(`${pointer.join('/') || '/'}: eq must be an object`); } - Object.entries(filter.eq).every(([key, value]) => - assertJSONValue(value, pointer.concat(key)), - ); + Object.entries(filter.eq).forEach(([key, value]) => { + assertKey(key, pointer); + assertJSONValue(value, pointer.concat(key)); + }); } function assertContainsFilter( @@ -371,9 +372,10 @@ function assertContainsFilter( if (typeof filter.contains !== 'object' || filter.contains == null) { throw new Error(`${pointer.join('/') || '/'}: contains must be an object`); } - Object.entries(filter.contains).every(([key, value]) => - assertJSONValue(value, pointer.concat(key)), - ); + Object.entries(filter.contains).forEach(([key, value]) => { + assertKey(key, pointer); + assertJSONValue(value, pointer.concat(key)); + }); } function assertRangeFilter( @@ -419,3 +421,15 @@ function assertRangeFilter( }); }); } + +export function assertKey(key: string, pointer: string[]) { + if (key.startsWith('[') && key.endsWith(']')) { + throw new Error( + `${pointer.join('/')}: field names cannot be wrapped in brackets: ${key}`, + ); + } +} + +export const parseQuery = (queryString: string) => { + return qs.parse(queryString, { depth: 10, strictDepth: true }); +}; diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index a97e27c1ec..e31ebd5b45 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -59,7 +59,7 @@ import { SupportedMimeType, lookupRouteTable, } from './router'; -import { assertQuery } from './query'; +import { assertQuery, parseQuery } from './query'; import type { Readable } from 'stream'; import { type CardDef } from 'https://cardstack.com/base/card-api'; import type * as CardAPI from 'https://cardstack.com/base/card-api'; @@ -76,7 +76,6 @@ import { fetcher } from './fetcher'; import { RealmIndexQueryEngine } from './realm-index-query-engine'; import { RealmIndexUpdater } from './realm-index-updater'; -import qs from 'qs'; import { MatrixBackendAuthentication, Utils, @@ -1548,7 +1547,7 @@ export class Realm { request.headers.get('X-Boxel-Building-Index'), ); - let cardsQuery = qs.parse(new URL(request.url).search.slice(1)); + let cardsQuery = parseQuery(new URL(request.url).search.slice(1)); assertQuery(cardsQuery); let doc = await this.#realmIndexQueryEngine.search(cardsQuery, { @@ -1572,7 +1571,8 @@ export class Realm { request.headers.get('X-Boxel-Building-Index'), ); - let parsedQueryString = qs.parse(new URL(request.url).search.slice(1)); + let href = new URL(request.url).search.slice(1); + let parsedQueryString = parseQuery(href); let htmlFormat = parsedQueryString.prerenderedHtmlFormat as string; let cardUrls = parsedQueryString.cardUrls as string[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4adcb682c..4656b5aaf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1317,8 +1317,8 @@ importers: specifier: ^0.0.30 version: 0.0.30 '@types/qs': - specifier: ^6.9.14 - version: 6.9.14 + specifier: ^6.9.17 + version: 6.9.17 '@types/qunit': specifier: ^2.11.3 version: 2.11.3 @@ -1590,8 +1590,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(prettier@3.1.0-dev) qs: - specifier: ^6.12.3 - version: 6.12.3 + specifier: ^6.13.0 + version: 6.13.0 qunit: specifier: ^2.20.0 version: 2.20.1 @@ -1728,8 +1728,8 @@ importers: specifier: ^8.11.5 version: 8.11.5 '@types/qs': - specifier: ^6.9.14 - version: 6.9.14 + specifier: ^6.9.17 + version: 6.9.17 '@types/qunit': specifier: ^2.11.3 version: 2.11.3 @@ -1815,8 +1815,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(prettier@3.1.0-dev) qs: - specifier: ^6.12.3 - version: 6.12.3 + specifier: ^6.13.0 + version: 6.13.0 qunit: specifier: ^2.20.0 version: 2.20.1 @@ -1932,8 +1932,8 @@ importers: specifier: ^0.0.30 version: 0.0.30 '@types/qs': - specifier: ^6.9.14 - version: 6.9.14 + specifier: ^6.9.17 + version: 6.9.17 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 @@ -1992,8 +1992,8 @@ importers: specifier: ^8.0.0 version: 8.0.0 qs: - specifier: ^6.12.3 - version: 6.12.3 + specifier: ^6.13.0 + version: 6.13.0 qunit: specifier: ^2.20.0 version: 2.20.1 @@ -6975,7 +6975,7 @@ packages: resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==} dependencies: '@types/node': 18.19.54 - '@types/qs': 6.9.14 + '@types/qs': 6.9.17 '@types/range-parser': 1.2.6 '@types/send': 0.17.3 dev: true @@ -6985,7 +6985,7 @@ packages: dependencies: '@types/body-parser': 1.19.4 '@types/express-serve-static-core': 4.17.39 - '@types/qs': 6.9.14 + '@types/qs': 6.9.17 '@types/serve-static': 1.15.4 dev: true @@ -7198,8 +7198,8 @@ packages: /@types/pluralize@0.0.30: resolution: {integrity: sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==} - /@types/qs@6.9.14: - resolution: {integrity: sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==} + /@types/qs@6.9.17: + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} /@types/qunit@2.11.3: resolution: {integrity: sha512-UF/4jDehcpRlMXzKXi2Z59Id48/uYlMeUDhYdjFrVVwy30Eud/e60Ok3yVjSPlHprafj3B315uFTrF6eqQTeSw==} @@ -8692,10 +8692,10 @@ packages: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 is-string: 1.0.7 dev: true @@ -8722,18 +8722,18 @@ packages: resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 dev: true /array.prototype.flat@1.3.2: resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 es-shim-unscopables: 1.0.0 @@ -8743,7 +8743,7 @@ packages: resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 es-shim-unscopables: 1.0.0 @@ -8792,7 +8792,7 @@ packages: /assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 is-nan: 1.3.2 object-is: 1.1.5 object.assign: 4.1.4 @@ -10828,7 +10828,7 @@ packages: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: function-bind: 1.1.2 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} @@ -14785,11 +14785,11 @@ packages: array-buffer-byte-length: 1.0.0 arraybuffer.prototype.slice: 1.0.2 available-typed-arrays: 1.0.5 - call-bind: 1.0.2 + call-bind: 1.0.7 es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 function.prototype.name: 1.1.6 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 get-symbol-description: 1.0.0 globalthis: 1.0.3 gopd: 1.0.1 @@ -14806,7 +14806,7 @@ packages: is-string: 1.0.7 is-typed-array: 1.1.12 is-weakref: 1.0.2 - object-inspect: 1.13.0 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.1 @@ -16473,7 +16473,7 @@ packages: dezalgo: 1.0.4 hexoid: 1.0.0 once: 1.4.0 - qs: 6.12.3 + qs: 6.13.0 dev: true /forwarded@0.2.0: @@ -17968,9 +17968,9 @@ packages: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} dependencies: - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 has: 1.0.4 - side-channel: 1.0.4 + side-channel: 1.0.6 /interpret@1.4.0: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} @@ -20400,9 +20400,6 @@ packages: resolution: {integrity: sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==} engines: {node: '>= 0.10.0'} - /object-inspect@1.13.0: - resolution: {integrity: sha512-HQ4J+ic8hKrgIt3mqk6cVOVrW2ozL4KdvHlqpBv9vDYWx9ysAgENAdvy4FoGF+KFdhR7nQTNm5J0ctAeOwn+3g==} - /object-inspect@1.13.2: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} @@ -20430,7 +20427,7 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 has-symbols: 1.0.3 object-keys: 1.1.1 @@ -20439,7 +20436,7 @@ packages: resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 dev: true @@ -20447,10 +20444,10 @@ packages: /object.groupby@1.0.1: resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 dev: true /object.pick@1.3.0: @@ -20464,7 +20461,7 @@ packages: resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 es-abstract: 1.22.2 dev: true @@ -20923,7 +20920,7 @@ packages: dependencies: is-ssh: 1.4.0 protocols: 1.4.8 - qs: 6.12.3 + qs: 6.13.0 query-string: 6.14.1 dev: true @@ -21672,8 +21669,8 @@ packages: side-channel: 1.0.6 dev: true - /qs@6.12.3: - resolution: {integrity: sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==} + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} dependencies: side-channel: 1.0.6 @@ -22038,7 +22035,7 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 define-properties: 1.2.1 set-function-name: 2.0.1 @@ -22848,9 +22845,9 @@ packages: /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.13.0 + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 /side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} @@ -23484,7 +23481,7 @@ packages: engines: {node: '>=12.*'} dependencies: '@types/node': 18.19.54 - qs: 6.12.3 + qs: 6.13.0 dev: true /style-loader@2.0.0(patch_hash=xqjji5denmqrswdovljl2t3yv4)(webpack@5.89.0): @@ -23530,7 +23527,7 @@ packages: formidable: 2.1.2 methods: 1.1.2 mime: 2.6.0 - qs: 6.12.3 + qs: 6.13.0 semver: 7.6.2 transitivePeerDependencies: - supports-color @@ -23960,7 +23957,7 @@ packages: faye-websocket: 0.11.4 livereload-js: 3.4.1 object-assign: 4.1.1 - qs: 6.12.3 + qs: 6.13.0 transitivePeerDependencies: - supports-color dev: true @@ -24373,7 +24370,7 @@ packages: /typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} dependencies: - qs: 6.12.3 + qs: 6.13.0 tunnel: 0.0.6 underscore: 1.13.6 dev: true @@ -24496,7 +24493,7 @@ packages: resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} engines: {node: '>= 0.8.0'} dependencies: - qs: 6.12.3 + qs: 6.13.0 dev: true /uniq@1.0.1: @@ -24687,7 +24684,7 @@ packages: resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} dependencies: punycode: 1.4.1 - qs: 6.12.3 + qs: 6.13.0 dev: true /use@3.1.1: