Repository: parse-community/Parse-SDK-JS
Pull request #718 (2019-01-11 19:23:15)

LiveQuery override data on update
Since the update / event subscription is the latest version of an object we should update the server data and localDatastore. Per [discussion](https://github.com/parse-community/Parse-SDK-JS/pull/714#issuecomment-451615779)

Added a isPinned helper to LDS.

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=h1) Report
> Merging [#718](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/15ece682cbb3a801ec4c3b15d0acbb11cea2bf47?src=pr&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718/graphs/tree.svg?width=650&token=8nCENM0QHj&height=150&src=pr)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #718      +/-   ##
==========================================
+ Coverage   88.59%   88.62%   +0.03%     
==========================================
  Files          53       53              
  Lines        4541     4554      +13     
  Branches     1054     1057       +3     
==========================================
+ Hits         4023     4036      +13     
  Misses        518      518
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseObject.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718/diff?src=pr&el=tree#diff-c3JjL1BhcnNlT2JqZWN0Lmpz) | `89.57% <100%> (+0.09%)` | :arrow_up: |
| [src/LiveQueryClient.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718/diff?src=pr&el=tree#diff-c3JjL0xpdmVRdWVyeUNsaWVudC5qcw==) | `87.86% <100%> (+0.36%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=footer). Last update [15ece68...37aae17](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/718?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dplewis: @flovilmart How does this look?

Patch:
diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js
index 46a4561..c9fdea8 100644
--- a/src/LiveQueryClient.js
+++ b/src/LiveQueryClient.js
@@ -9,6 +9,7 @@
  */
 /* global WebSocket */
 
+import CoreManager from './CoreManager';
 import EventEmitter from './EventEmitter';
 import ParseObject from './ParseObject';
 import LiveQuerySubscription from './LiveQuerySubscription';
@@ -392,7 +393,9 @@ class LiveQueryClient extends EventEmitter {
       if (!subscription) {
         break;
       }
+      let override = false;
       if (data.original) {
+        override = true;
         delete data.original.__type;
         // Check for removed fields
         for (const field in data.original) {
@@ -403,9 +406,14 @@ class LiveQueryClient extends EventEmitter {
         data.original = ParseObject.fromJSON(data.original, false);
       }
       delete data.object.__type;
-      const parseObject = ParseObject.fromJSON(data.object, false);
+      const parseObject = ParseObject.fromJSON(data.object, override);
 
       subscription.emit(data.op, parseObject, data.original);
+
+      const localDatastore = CoreManager.getLocalDatastore();
+      if (override && localDatastore.isEnabled) {
+        localDatastore._updateObjectIfPinned(parseObject).then(() => {});
+      }
     }
     }
   }
diff --git a/src/ParseObject.js b/src/ParseObject.js
index faf0b2a..913cd1f 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -1228,6 +1228,25 @@ class ParseObject {
     return ParseObject.unPinAllWithName(localDatastore.DEFAULT_PIN, [this]);
   }
 
+  /**
+   * Asynchronously returns if the object is pinned
+   *
+   * <pre>
+   * const isPinned = await object.isPinned();
+   * </pre>
+   */
+  async isPinned(): Promise {
+    const localDatastore = CoreManager.getLocalDatastore();
+    if (localDatastore.checkIfEnabled()) {
+      const objectKey = localDatastore.getKeyForObject(this);
+      const pin = await localDatastore.fromPinWithName(objectKey);
+      if (pin) {
+        return Promise.resolve(true);
+      }
+    }
+    return Promise.resolve(false);
+  }
+
   /**
    * Asynchronously stores the objects and every object they point to in the local datastore, recursively.
    *
@@ -1253,6 +1272,8 @@ class ParseObject {
    * <pre>
    * await object.unPinWithName(name);
    * </pre>
+   *
+   * @param {String} name Name of Pin.
    */
   unPinWithName(name: string): Promise {
     return ParseObject.unPinAllWithName(name, [this]);


Test patch:
diff --git a/integration/test/ParseLocalDatastoreTest.js b/integration/test/ParseLocalDatastoreTest.js
index f81af6c..4d7251c 100644
--- a/integration/test/ParseLocalDatastoreTest.js
+++ b/integration/test/ParseLocalDatastoreTest.js
@@ -81,6 +81,23 @@ function runTest(controller) {
       assert.equal(cachedObject.field, 'new info');
     });
 
+    it(`${controller.name} can check if pinned`, async () => {
+      const object = new TestObject();
+      object.set('field', 'test');
+      await object.save();
+
+      let isPinned = await object.isPinned();
+      assert.equal(isPinned, false);
+
+      await object.pin();
+      isPinned = await object.isPinned();
+      assert.equal(isPinned, true);
+
+      await object.unPin();
+      isPinned = await object.isPinned();
+      assert.equal(isPinned, false);
+    });
+
     it(`${controller.name} can pin (recursive)`, async () => {
       const parent = new TestObject();
       const child = new Item();
diff --git a/src/__tests__/LiveQueryClient-test.js b/src/__tests__/LiveQueryClient-test.js
index 2c2685f..d8e04e4 100644
--- a/src/__tests__/LiveQueryClient-test.js
+++ b/src/__tests__/LiveQueryClient-test.js
@@ -33,14 +33,29 @@ jest.dontMock('../unsavedChildren');
 jest.dontMock('../ParseACL');
 jest.dontMock('../ParseQuery');
 jest.dontMock('../LiveQuerySubscription');
+jest.dontMock('../LocalDatastore');
+
 jest.useFakeTimers();
 
+const mockLocalDatastore = {
+  isEnabled: false,
+  _updateObjectIfPinned: jest.fn(),
+};
+jest.setMock('../LocalDatastore', mockLocalDatastore);
+
+const CoreManager = require('../CoreManager');
 const LiveQueryClient = require('../LiveQueryClient').default;
 const ParseObject = require('../ParseObject').default;
 const ParseQuery = require('../ParseQuery').default;
 const events = require('events');
 
+CoreManager.setLocalDatastore(mockLocalDatastore);
+
 describe('LiveQueryClient', () => {
+  beforeEach(() => {
+    mockLocalDatastore.isEnabled = false;
+  });
+
   it('can connect to server', () => {
     const liveQueryClient = new LiveQueryClient({
       applicationId: 'applicationId',
@@ -264,6 +279,63 @@ describe('LiveQueryClient', () => {
     expect(isChecked).toBe(true);
   });
 
+  it('can handle WebSocket response override data on update', () => {
+    const liveQueryClient = new LiveQueryClient({
+      applicationId: 'applicationId',
+      serverURL: 'ws://test',
+      javascriptKey: 'javascriptKey',
+      masterKey: 'masterKey',
+      sessionToken: 'sessionToken'
+    });
+    // Add mock subscription
+    const subscription = new events.EventEmitter();
+    liveQueryClient.subscriptions.set(1, subscription);
+    const object = new ParseObject('Test');
+    const original = new ParseObject('Test');
+    object.set('key', 'value');
+    original.set('key', 'old');
+    const data = {
+      op: 'update',
+      clientId: 1,
+      requestId: 1,
+      object: object._toFullJSON(),
+      original: original._toFullJSON(),
+    };
+    const event = {
+      data: JSON.stringify(data)
+    }
+
+    jest.spyOn(
+      mockLocalDatastore,
+      '_updateObjectIfPinned'
+    )
+      .mockImplementationOnce(() => Promise.resolve());
+
+    const spy = jest.spyOn(
+      ParseObject,
+      'fromJSON'
+    )
+      .mockImplementationOnce(() => original)
+      .mockImplementationOnce(() => object);
+
+
+    mockLocalDatastore.isEnabled = true;
+
+    let isChecked = false;
+    subscription.on('update', () => {
+      isChecked = true;
+    });
+
+    liveQueryClient._handleWebSocketMessage(event);
+    const override = true;
+
+    expect(ParseObject.fromJSON.mock.calls[1][1]).toEqual(override);
+    expect(mockLocalDatastore._updateObjectIfPinned).toHaveBeenCalledTimes(1);
+
+    expect(isChecked).toBe(true);
+    spy.mockRestore();
+  });
+
   it('can handle WebSocket response unset field', async () => {
     const liveQueryClient = new LiveQueryClient({
       applicationId: 'applicationId',
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 6c95b17..b4232fb 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -2639,6 +2639,22 @@ describe('ParseObject pin', () => {
     expect(mockLocalDatastore._handleUnPinWithName).toHaveBeenCalledWith('test_pin', object);
   });
 
+  it('can check if pinned', async () => {
+    const object = new ParseObject('Item');
+    object.id = '1234';
+    mockLocalDatastore
+      .fromPinWithName
+      .mockImplementationOnce(() => {
+        return { 'Item_1234': object._toFullJSON() }
+      })
+      .mockImplementationOnce(() => null);
+
+    let isPinned = await object.isPinned();
+    expect(isPinned).toEqual(true);
+    isPinned = await object.isPinned();
+    expect(isPinned).toEqual(false);
+  });
+
   it('can fetchFromLocalDatastore', async () => {
     const object = new ParseObject('Item');
     object.id = '123';
@@ -2704,6 +2720,7 @@ describe('ParseObject pin', () => {
     const obj = new ParseObject('Item');
     await obj.pin();
     await obj.unPin();
+    await obj.isPinned();
     await obj.pinWithName(name);
     await obj.unPinWithName(name);
     await obj.fetchFromLocalDatastore();
@@ -2715,7 +2732,7 @@ describe('ParseObject pin', () => {
     await ParseObject.unPinAllObjects();
     await ParseObject.unPinAllObjectsWithName(name);
 
-    expect(spy).toHaveBeenCalledTimes(11);
+    expect(spy).toHaveBeenCalledTimes(12);
     expect(spy).toHaveBeenCalledWith('Parse.enableLocalDatastore() must be called first');
     spy.mockRestore();
   });