Repository: parse-community/Parse-SDK-JS
Pull request #1839 (2023-03-30 20:53:49)

feat: Add `ParseQuery.watch` to trigger LiveQuery only on update of specific fields
## Pull Request

- Report security issues [confidentially](https://github.com/parse-community/Parse-SDK-JS/security/policy).
- Any contribution is under this [license](https://github.com/parse-community/Parse-SDK-JS/blob/alpha/LICENSE).
- Link this pull request to an [issue](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).

## Issue
<!-- Add the link to the issue that this PR closes. -->
LiveQuery watch was introducted in Parse Server 6 https://github.com/parse-community/parse-server/pull/8028

Closes: https://github.com/parse-community/Parse-SDK-JS/issues/1806


parse-github-assistant[bot]: 
### Thanks for opening this pull request!

<!-- parse-issue-bot-meta-tag-id {"suggestPr":false,"thanksFeature":false,"thanksPr":false} -->


codecov[bot]: ## [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`f30c3ab`)](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/f30c3aba81be9bdc8a1cceb3fa21d5124e794d15?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) 99.90% compared to head [(`5ae1116`)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) 99.90%.

> :exclamation: Current head 5ae1116 differs from pull request most recent head 30e6b55. Consider uploading reports for the commit 30e6b55 to get more accurate results

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


```diff
@@           Coverage Diff           @@
##            alpha    #1839   +/-   ##
=======================================
  Coverage   99.90%   99.90%           
=======================================
  Files          61       61           
  Lines        6104     6117   +13     
  Branches     1482     1485    +3     
=======================================
+ Hits         6098     6111   +13     
  Misses          6        6           
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community) | Coverage Δ | |
|---|---|---|
| [src/LiveQueryClient.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=parse-community#diff-c3JjL0xpdmVRdWVyeUNsaWVudC5qcw==) | `99.04% <100.00%> (+<0.01%)` | :arrow_up: |
| [src/ParseQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?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%> (ø)` | |

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 in Codecov by Sentry](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1839?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).


Patch:
diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js
index 893ba36..6baf67f 100644
--- a/src/LiveQueryClient.js
+++ b/src/LiveQueryClient.js
@@ -191,7 +191,8 @@ class LiveQueryClient extends EventEmitter {
     const className = query.className;
     const queryJSON = query.toJSON();
     const where = queryJSON.where;
-    const fields = queryJSON.keys ? queryJSON.keys.split(',') : undefined;
+    const fields = queryJSON.keys?.split(',');
+    const watch = queryJSON.watch?.split(',');
     const subscribeRequest = {
       op: OP_TYPES.SUBSCRIBE,
       requestId: this.requestId,
@@ -199,6 +200,7 @@ class LiveQueryClient extends EventEmitter {
         className,
         where,
         fields,
+        watch,
       },
     };
 
diff --git a/src/ParseQuery.js b/src/ParseQuery.js
index 7c7ce8b..f2e18b4 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.js
@@ -22,6 +22,7 @@ export type WhereClause = {
 
 export type QueryJSON = {
   where: WhereClause,
+  watch?: string,
   include?: string,
   excludeKeys?: string,
   keys?: string,
@@ -224,6 +225,7 @@ class ParseQuery {
    */
   className: string;
   _where: any;
+  _watch: Array<string>;
   _include: Array<string>;
   _exclude: Array<string>;
   _select: Array<string>;
@@ -265,6 +267,7 @@ class ParseQuery {
     }
 
     this._where = {};
+    this._watch = [];
     this._include = [];
     this._exclude = [];
     this._count = false;
@@ -426,6 +429,9 @@ class ParseQuery {
       where: this._where,
     };
 
+    if (this._watch.length) {
+      params.watch = this._watch.join(',');
+    }
     if (this._include.length) {
       params.include = this._include.join(',');
     }
@@ -495,6 +501,10 @@ class ParseQuery {
       this._where = json.where;
     }
 
+    if (json.watch) {
+      this._watch = json.watch.split(',');
+    }
+
     if (json.include) {
       this._include = json.include.split(',');
     }
@@ -1921,6 +1931,25 @@ class ParseQuery {
     return this;
   }
 
+  /**
+   * Restricts live query to trigger only for watched fields.
+   *
+   * Requires Parse Server 6.0.0+
+   *
+   * @param {...string|Array<string>} keys The name(s) of the key(s) to watch.
+   * @returns {Parse.Query} Returns the query, so you can chain this call.
+   */
+  watch(...keys: Array<string | Array<string>>): ParseQuery {
+    keys.forEach(key => {
+      if (Array.isArray(key)) {
+        this._watch = this._watch.concat(key);
+      } else {
+        this._watch.push(key);
+      }
+    });
+    return this;
+  }
+
   /**
    * Changes the read preference that the backend will use when performing the query to the database.
    *


Test patch:
diff --git a/integration/test/ParseLiveQueryTest.js b/integration/test/ParseLiveQueryTest.js
index dd7d607..c33f21a 100644
--- a/integration/test/ParseLiveQueryTest.js
+++ b/integration/test/ParseLiveQueryTest.js
@@ -76,7 +76,7 @@ describe('Parse LiveQuery', () => {
     query.equalTo('objectId', object.id);
     const subscription = await client.subscribe(query);
     const promise = resolvingPromise();
-    subscription.on('update', async (object) => {
+    subscription.on('update', async object => {
       assert.equal(object.get('foo'), 'bar');
       await client.close();
       promise.resolve();
@@ -206,7 +206,7 @@ describe('Parse LiveQuery', () => {
     subscription.on('update', async object => {
       assert.equal(object.get('foo'), 'bar');
       await Parse.User.logOut();
-      promise.resolve()
+      promise.resolve();
     });
     await object.save({ foo: 'bar' });
     await promise;
@@ -276,6 +276,44 @@ describe('Parse LiveQuery', () => {
     await promise;
   });
 
+  it('can subscribe to query with watch', async () => {
+    const query = new Parse.Query(TestObject);
+    query.watch('yolo');
+    const subscription = await query.subscribe();
+    const spy = {
+      create(obj) {
+        if (!obj.get('yolo')) {
+          fail('create should not have been called');
+        }
+      },
+      update(object, original) {
+        if (object.get('yolo') === original.get('yolo')) {
+          fail('create should not have been called');
+        }
+      },
+    };
+    const createSpy = spyOn(spy, 'create').and.callThrough();
+    const updateSpy = spyOn(spy, 'update').and.callThrough();
+    subscription.on('create', spy.create);
+    subscription.on('update', spy.update);
+    const obj = new TestObject();
+    obj.set('foo', 'bar');
+    await obj.save();
+    obj.set('foo', 'xyz');
+    obj.set('yolo', 'xyz');
+    await obj.save();
+    const obj2 = new TestObject();
+    obj2.set('foo', 'bar');
+    obj2.set('yolo', 'bar');
+    await obj2.save();
+    obj2.set('foo', 'bart');
+    await obj2.save();
+    await sleep(1000);
+    await subscription.unsubscribe();
+    expect(createSpy).toHaveBeenCalledTimes(1);
+    expect(updateSpy).toHaveBeenCalledTimes(1);
+  });
+
   it('live query can handle beforeConnect and beforeSubscribe errors', async () => {
     await reconfigureServer({
       cloud({ Cloud }) {
diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js
index 622f9bb..4499b27 100644
--- a/src/__tests__/ParseQuery-test.js
+++ b/src/__tests__/ParseQuery-test.js
@@ -1078,6 +1078,32 @@ describe('ParseQuery', () => {
     expect(q2._exclude).toEqual(['foo', 'bar']);
   });
 
+  it('can watch keys', () => {
+    const q = new ParseQuery('Item');
+    q.watch('foo');
+    const json = q.toJSON();
+    expect(json).toEqual({
+      where: {},
+      watch: 'foo',
+    });
+    const q2 = new ParseQuery('Item');
+    q2.withJSON(json);
+    expect(q2._watch).toEqual(['foo']);
+  });
+
+  it('can watch multiple keys', () => {
+    const q = new ParseQuery('Item');
+    q.watch(['foo', 'bar']);
+    const json = q.toJSON();
+    expect(json).toEqual({
+      where: {},
+      watch: 'foo,bar',
+    });
+    const q2 = new ParseQuery('Item');
+    q2.withJSON(json);
+    expect(q2._watch).toEqual(['foo', 'bar']);
+  });
+
   it('can use extraOptions', () => {
     const q = new ParseQuery('Item');
     q._extraOptions.randomOption = 'test';
@@ -2334,8 +2360,7 @@ describe('ParseQuery', () => {
 
     const q = new ParseQuery('Thing');
     let testObject;
-    q
-      .find()
+    q.find()
       .then(results => {
         testObject = results[0];
 
@@ -2460,8 +2485,7 @@ describe('ParseQuery', () => {
 
     const q = new ParseQuery('Thing');
     let testObject;
-    q
-      .first()
+    q.first()
       .then(result => {
         testObject = result;
 
@@ -2875,8 +2899,7 @@ describe('ParseQuery', () => {
     const q = new ParseQuery('Thing');
     q.select('other', 'tbd', 'subObject.key1');
     let testObject;
-    q
-      .find()
+    q.find()
       .then(results => {
         testObject = results[0];
 
@@ -2926,8 +2949,7 @@ describe('ParseQuery', () => {
 
     const q = new ParseQuery('Thing');
     let testObject;
-    q
-      .find()
+    q.find()
       .then(results => {
         testObject = results[0];