Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qs.parse returns invalid key with brackets when using nested array query #1751

Merged
merged 13 commits into from
Nov 6, 2024

Conversation

tintinthong
Copy link
Contributor

@tintinthong tintinthong commented Nov 4, 2024

EDIT TLDR:

This PR fixes a limitation of the depth that qs.parse is parsing. There is a default of depthLimit = 5 hence, our nested queries would parse key names with brackets outside. This PR configures, depthLimit = 10 and sets strictDepth = true ( a new feature shipped in qs) which errors out if qs.parse recurses too deep.

=====Below Description here is historical=====

Problem

When making complicated queries where any array nests within every array, qs.parse incorrectly parses the string st a key has brackets wrapped around it.

Fixing this problem unlocks the ability to nest every and any queries which is useful for complex querying in applications that will be used by ecosystem team.

    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' },
        },
      ],
    };

Here is the test you can see a3ea344

logger.ts:97 Error: Your filter refers to nonexistent field "[author" on type {"module":"http://test-realm/test/book","name":"Book"}
    at IndexQueryEngine.walkFilterFieldPath (index-query-engine.ts:941:15)
    at async IndexQueryEngine.handleFieldArity (index-query-engine.ts:757:31)
    at async Promise.all (:4200/index 52)
    at async IndexQueryEngine.makeExpression (index-query-engine.ts:698:7)
    at async IndexQueryEngine.queryCards (index-query-engine.ts:146:23)
    at async Promise.all (:4200/index 1)

The cardsQuery after returned from qs.parse will look like this

{
  "filter": {
    "on": { "module": "http://test-realm/test/book", "name": "Book" },
    "every": [
      { "eq": { "author.firstName": "Cardy" } },
      {
        "any": [
          { "eq": { "[author.lastName]": "Jones" } },   <- PROBLEM should be 'author.lastName'
          { "eq": { "[author.lastName]": "Stackington Jr. III" } } <- PROBLEM should be 'author.lastName'
        ]
      }
    ]
  },
  "sort": [
    {
      "by": "author.lastName",
      "on": { "module": "http://test-realm/test/book", "name": "Book" }
    }
  ]
}

The issue errors out because pathSegments doesn't know how to handle brackets

let pathSegments = path.split('.');

SOLUTION

I decided to post-parse the qs.parse object even before going into the realm-index.query-engine. Placing my fix in realm.ts was intentional because I thought that the issue was within qs.parse, although, its possibly easier if using one-liner removeOuterBracket just occured near pathSegments inside realm.ts (open to opinions here) since the recursion is already made.

I have also attempted to use qs.parse configuration but was unsuccesful bcos I don't think it works out-of-the-box

  • allowDots configuration but it totally breaks the query structure query-engine is expecting since it converts keys to nested objects.
  • The other one is to use my own custom decoder but qs.parse only allows decoding of one-level deep and I didn't think it was worthwhile to expose myself to a recursive qs.parse

@tintinthong tintinthong changed the title Remove brackets for qs.parse Remove brackets returned by qs.parse when searching nested array query Nov 4, 2024
Copy link

github-actions bot commented Nov 4, 2024

Host Test Results

    1 files  ±0      1 suites  ±0   20m 41s ⏱️ -57s
678 tests +2  677 ✔️ +2  1 💤 ±0  0 ±0 
683 runs  +2  682 ✔️ +2  1 💤 ±0  0 ±0 

Results for commit eaedefa. ± Comparison against base commit 6feb97a.

♻️ This comment has been updated with latest results.

@tintinthong tintinthong changed the title Remove brackets returned by qs.parse when searching nested array query qs.parse returns invalid key with brackets when using nested array query Nov 4, 2024
Comment on lines -310 to -312
filter.every.every((value: any, index: number) =>
assertFilter(value, pointer.concat(`[${index}]`)),
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.every is problematic because it expects to return a boolean. So, not all assertions are being run thru

@@ -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)),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointer.concat doesn't even mutate the pointer. I would prefer to handle in another PR

@@ -414,4 +414,70 @@ module(`Integration | prerendered-card-search`, function (hooks) {
.dom('.card-container:nth-child(1)')
.containsText('Cardy Stackington Jr. III');
});

test<TestContextWithSSE>(`can parse objects in nested array`, async function (assert) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to come up with a better name here

@habdelra
Copy link
Contributor

habdelra commented Nov 4, 2024

I noticed in the qs docs, this comment:

By default, when nesting objects qs will only parse up to 5 children deep.

Is this actually the problem, do these items reside 5+ levels deep? if they were higher up in the composition hierarchy would they be parsed correctly?

If so, there is a depth configuration in qs that will allow you to parse even deeper, and there is a strictDepth config that will throw when you try to go deeper than the config so that you'll get a clear error about what is actually wrong instead of an error on the side effect of what went wrong.

@tintinthong
Copy link
Contributor Author

s this actually the problem, do these items reside 5+ levels deep? if they were higher up in the composition hierarchy would they be parsed correctly?

Yes it is! I recall looking into this and thought 5 was sufficient depth. The depth that works is 6! I think this solves my issue, any ideas what should be the default depth?

@habdelra
Copy link
Contributor

habdelra commented Nov 5, 2024

s this actually the problem, do these items reside 5+ levels deep? if they were higher up in the composition hierarchy would they be parsed correctly?

Yes it is! I recall looking into this and thought 5 was sufficient depth. The depth that works is 6! I think this solves my issue, any ideas what should be the default depth?

Let’s do a strictDepth of 10. If we go beyond that, we’ll at least get a meaningful error message.

@tintinthong
Copy link
Contributor Author

tintinthong commented Nov 5, 2024

}

export const parseQuery = (queryString: string) => {
// @ts-ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's be a little more precise why we are ignoring. I usually do:

// @ts-expect-error no types available for XXX

this way when types do become available our lint will let us know that the following line doesn't need to be ignored anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Just fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been merged DefinitelyTyped/DefinitelyTyped#71087 and deployed on @6.9.17 of qs. No need for ts-expect-error anymore

@tintinthong tintinthong merged commit e8a6ec9 into main Nov 6, 2024
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants