Repository: parse-community/Parse-SDK-JS
Pull request #1666 (2023-01-25 00:12:20)

fix: Local datastore query with `containedIn` not working when field is an array
### New Pull Request Checklist
<!--
    Check every following box [x] before submitting your PR.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am creating this PR in reference to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

### Issue Description
<!-- Add a brief description of the issue this PR solves. -->

Similar to https://github.com/parse-community/parse-server/pull/8128

Closes: Added additional test case for https://github.com/parse-community/parse-server/issues/8335

### Approach
<!-- Add a description of the approach in this PR. -->

### TODOs before merging
<!--
    Add TODOs that need to be completed before merging this PR.
    Delete TODOs that do not apply to this PR.
-->

- [x] Add tests
- [x] Add changes to documentation (guides, repository pages, in-code descriptions)


parse-github-assistant[bot]: I will reformat the title to use the proper commit message syntax.
parse-github-assistant[bot]: 
### Thanks for opening this pull request!

- ❌ Please link an issue that describes the reason for this pull request, otherwise your pull request will be closed. Make sure to write it as `Closes: #123` in the PR description, so I can recognize it.

<!-- parse-issue-bot-meta-tag-id {"requireLinkedIssue":true} -->


codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) Report
Base: **99.89**% // Head: **99.89**% // Increases project coverage by **`+0.00%`** :tada:
> Coverage data is based on head [(`1c56ae4`)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) compared to base [(`0b2985c`)](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/0b2985c0588cf410353f28afca174a84c06473c8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##            alpha    #1666   +/-   ##
=======================================
  Coverage   99.89%   99.89%           
=======================================
  Files          61       61           
  Lines        5973     5977    +4     
  Branches     1367     1369    +2     
=======================================
+ Hits         5967     5971    +4     
  Misses          6        6           
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) | Coverage Δ | |
|---|---|---|
| [src/OfflineQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community#diff-c3JjL09mZmxpbmVRdWVyeS5qcw==) | `99.68% <100.00%> (+<0.01%)` | :arrow_up: |
| [src/ParseQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community#diff-c3JjL1BhcnNlUXVlcnkuanM=) | `100.00% <100.00%> (ø)` | |
| [src/StorageController.react-native.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community#diff-c3JjL1N0b3JhZ2VDb250cm9sbGVyLnJlYWN0LW5hdGl2ZS5qcw==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1666?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community).

mtrezza: Thanks for the PR; could you rephrase the PR title to make it easier to understand in the changelog what this actually fixes?

dplewis: @mtrezza The LocalDatastore is internally code copied from LiveQuery meaning queries that LiveQuery supports the LocalDatastore should support too. Those are just additional test cases that doesn't exist server side.
mtrezza: How would you best describe the fix in `src/OfflineQuery.js`?

> Local datastore not working properly for `Parse.Query.containedIn` where field is an array
dplewis: > How would you best describe the fix in `src/OfflineQuery.js`?

It's the same fix as the `QueryTools.js` in the latest 5.4.0 release of the server

https://github.com/parse-community/parse-server/pull/8128/files#diff-13c52f9da4d37c7dc0bd3e9e845716de38a13e2a8632465ea167422762d5a9b6
dplewis: > Or something more specific?

For non array fields you are comparing a single value against an array. This fix checks a array against an array recursively. As I write this I realized that `nonContainedIn` is broken and will return always return an object.

Without this fix we would have to rely on.

`haystack.indexOf(needle) > -1`

Array.indexOf(Array) will always return -1

mtrezza: The other PR is called "fix: liveQuery with containedIn not working when object field is an array".

How about this:

> fix: Local datastore query with `containedIn` not working when field is an array

dplewis: @mtrezza I write a few additional test cases, I also think there might be an issue with `containedBy` queries.
dplewis: @mtrezza This is ready for review
mtrezza: Re-running CI

I'm curious about these fails: https://github.com/parse-community/Parse-SDK-JS/actions/runs/4019619971/jobs/6906679057; it seems a connection error, do you have any idea what that could be since you've been looking at the flaky tests?
dplewis: @mtrezza I suspect it might be an issue with `reconfigureServer`. `allowCustomObjectId` tests seem most common but I have seen `eventuallyQueue` tests also fail with his error.

Patch:
diff --git a/src/OfflineQuery.js b/src/OfflineQuery.js
index f76cfa0..aac8b15 100644
--- a/src/OfflineQuery.js
+++ b/src/OfflineQuery.js
@@ -24,6 +24,13 @@ function contains(haystack, needle) {
     }
     return false;
   }
+  if (Array.isArray(needle)) {
+    for (const need of needle) {
+      if (contains(haystack, need)) {
+        return true;
+      }
+    }
+  }
   return haystack.indexOf(needle) > -1;
 }
 


Test patch:
diff --git a/integration/test/ParseLocalDatastoreTest.js b/integration/test/ParseLocalDatastoreTest.js
index 19d2245..2c13c24 100644
--- a/integration/test/ParseLocalDatastoreTest.js
+++ b/integration/test/ParseLocalDatastoreTest.js
@@ -1227,6 +1227,66 @@ function runTest(controller) {
       assert.equal(results[0].id, parent3.id);
     });
 
+    it(`${controller.name} can handle containsAll query on array`, async () => {
+      const obj1 = new TestObject({ arrayField: [1, 2, 3, 4] });
+      const obj2 = new TestObject({ arrayField: [0, 2] });
+      const obj3 = new TestObject({ arrayField: [1, 2, 3] });
+      await Parse.Object.saveAll([obj1, obj2, obj3]);
+      await Parse.Object.pinAll([obj1, obj2, obj3]);
+
+      let query = new Parse.Query(TestObject);
+      query.containsAll('arrayField', [1, 2]);
+      query.fromPin();
+      let results = await query.find();
+      expect(results.length).toBe(2);
+
+      query = new Parse.Query(TestObject);
+      query.containsAll('arrayField', [5, 6]);
+      query.fromPin();
+      results = await query.find();
+      expect(results.length).toBe(0);
+    });
+
+    it(`${controller.name} can handle containedIn query on array`, async () => {
+      const obj1 = new TestObject({ arrayField: [1, 2, 3, 4] });
+      const obj2 = new TestObject({ arrayField: [0, 2] });
+      const obj3 = new TestObject({ arrayField: [1, 2, 3] });
+      await Parse.Object.saveAll([obj1, obj2, obj3]);
+      await Parse.Object.pinAll([obj1, obj2, obj3]);
+
+      let query = new Parse.Query(TestObject);
+      query.containedIn('arrayField', [3]);
+      query.fromPin();
+      let results = await query.find();
+      expect(results.length).toEqual(2);
+
+      query = new Parse.Query(TestObject);
+      query.containedIn('arrayField', [5]);
+      query.fromPin();
+      results = await query.find();
+      expect(results.length).toEqual(0);
+    });
+
+    it(`${controller.name} can handle notContainedIn query on array`, async () => {
+      const obj1 = new TestObject({ arrayField: [1, 2, 3, 4] });
+      const obj2 = new TestObject({ arrayField: [0, 2] });
+      const obj3 = new TestObject({ arrayField: [1, 2, 3] });
+      await Parse.Object.saveAll([obj1, obj2, obj3]);
+      await Parse.Object.pinAll([obj1, obj2, obj3]);
+
+      let query = new Parse.Query(TestObject);
+      query.notContainedIn('arrayField', [3]);
+      query.fromPin();
+      let results = await query.find();
+      expect(results.length).toEqual(1);
+
+      query = new Parse.Query(TestObject);
+      query.notContainedIn('arrayField', [5]);
+      query.fromPin();
+      results = await query.find();
+      expect(results.length).toEqual(3);
+    });
+
     it(`${controller.name} can test equality with undefined`, async () => {
       const query = new Parse.Query('BoxedNumber');
       query.equalTo('number', undefined);
diff --git a/src/__tests__/OfflineQuery-test.js b/src/__tests__/OfflineQuery-test.js
index ae67a84..1a24bc0 100644
--- a/src/__tests__/OfflineQuery-test.js
+++ b/src/__tests__/OfflineQuery-test.js
@@ -610,6 +610,33 @@ describe('OfflineQuery', () => {
     expect(matchesQuery(q.className, message, [], q)).toBe(false);
   });
 
+  it('should support containedIn with array of pointers', () => {
+    const profile1 = new ParseObject('Profile');
+    profile1.id = 'yeahaw';
+    const profile2 = new ParseObject('Profile');
+    profile2.id = 'yes';
+
+    const message = new ParseObject('Message');
+    message.id = 'O2';
+    message.set('profiles', [profile1, profile2]);
+
+    let q = new ParseQuery('Message');
+    q.containedIn('profiles', [
+      ParseObject.fromJSON({ className: 'Profile', objectId: 'no' }),
+      ParseObject.fromJSON({ className: 'Profile', objectId: 'yes' }),
+    ]);
+
+    expect(matchesQuery(q.className, message, [], q)).toBe(true);
+
+    q = new ParseQuery('Message');
+    q.containedIn('profiles', [
+      ParseObject.fromJSON({ className: 'Profile', objectId: 'no' }),
+      ParseObject.fromJSON({ className: 'Profile', objectId: 'nope' }),
+    ]);
+
+    expect(matchesQuery(q.className, message, [], q)).toBe(false);
+  });
+
   it('should support notContainedIn with pointers', () => {
     const profile = new ParseObject('Profile');
     profile.id = 'abc';