Repository: parse-community/Parse-SDK-JS
Pull request #795 (2019-04-13 13:33:29)

 LDS: Improve pinning unsaved objects
Closes: #792

Saves `_localId` into the LocalDatastore to keep consistency when querying.

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

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

```diff
@@            Coverage Diff             @@
##           master     #795      +/-   ##
==========================================
+ Coverage   92.01%   92.02%   +<.01%     
==========================================
  Files          54       54              
  Lines        4909     4914       +5     
  Branches     1087     1089       +2     
==========================================
+ Hits         4517     4522       +5     
  Misses        392      392
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/795?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/795/diff?src=pr&el=tree#diff-c3JjL1BhcnNlUXVlcnkuanM=) | `96.03% <100%> (+0.01%)` | :arrow_up: |
| [src/LocalDatastore.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/795/diff?src=pr&el=tree#diff-c3JjL0xvY2FsRGF0YXN0b3JlLmpz) | `98.68% <100%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/795?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/795?src=pr&el=footer). Last update [05e8c79...9308f03](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/795?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dplewis: @acinader This PR allows you to updated pinned objects that aren't saved. We find unsaved object by _localId.
dplewis: @acinader It I revert package-lock.json the tests fail [travis-ci](https://travis-ci.org/parse-community/Parse-SDK-JS/builds/522248550?utm_source=github_status&utm_medium=notification)

Patch:
diff --git a/integration/server.js b/integration/server.js
index 967028b..5f440b8 100644
--- a/integration/server.js
+++ b/integration/server.js
@@ -22,7 +22,9 @@ const api = new ParseServer({
     facebook: {
       appIds: "test"
     }
-  }
+  },
+  verbose: false,
+  silent: true,
 });
 
 app.use('/parse', api);
diff --git a/src/LocalDatastore.js b/src/LocalDatastore.js
index 867c225..8062a21 100644
--- a/src/LocalDatastore.js
+++ b/src/LocalDatastore.js
@@ -79,7 +79,11 @@ const LocalDatastore = {
     for (const parent of objects) {
       const children = this._getChildren(parent);
       const parentKey = this.getKeyForObject(parent);
-      children[parentKey] = parent._toFullJSON();
+      const json = parent._toFullJSON();
+      if (parent._localId) {
+        json._localId = parent._localId;
+      }
+      children[parentKey] = json;
       for (const objectKey in children) {
         objectKeys.push(objectKey);
         toPinPromises.push(this.pinWithName(objectKey, [children[objectKey]]));
@@ -338,6 +342,9 @@ const LocalDatastore = {
     for (const key of keys) {
       // Ignore the OBJECT_PREFIX
       const [ , , className, objectId] = key.split('_');
+      if (objectId.startsWith('local')) {
+        continue;
+      }
       if (!(className in pointersHash)) {
         pointersHash[className] = new Set();
       }
diff --git a/src/ParseQuery.js b/src/ParseQuery.js
index b8dd6d0..9ba16c3 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.js
@@ -324,7 +324,9 @@ class ParseQuery {
     const objects = await localDatastore._serializeObjectsFromPinName(this._localDatastorePinName);
     let results = objects.map((json, index, arr) => {
       const object = ParseObject.fromJSON(json, false);
-
+      if (json._localId && !json.objectId) {
+        object._localId = json._localId;
+      }
       if (!OfflineQuery.matchesQuery(this.className, object, arr, this)) {
         return null;
       }


Test patch:
diff --git a/integration/test/ParseLocalDatastoreTest.js b/integration/test/ParseLocalDatastoreTest.js
index cbf9c24..fd2ea6a 100644
--- a/integration/test/ParseLocalDatastoreTest.js
+++ b/integration/test/ParseLocalDatastoreTest.js
@@ -16,7 +16,13 @@ const PIN_PREFIX = LocalDatastoreUtils.PIN_PREFIX;
 function LDS_KEY(object) {
   return Parse.LocalDatastore.getKeyForObject(object);
 }
-
+function LDS_FULL_JSON(object) {
+  const json = object._toFullJSON();
+  if (object._localId) {
+    json._localId = object._localId;
+  }
+  return json;
+}
 function runTest(controller) {
   describe(`Parse Object Pinning (${controller.name})`, () => {
     beforeEach(async () => {
@@ -75,7 +81,7 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 2);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(object)]);
-      assert.deepEqual(localDatastore[LDS_KEY(object)], [object._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(object)], [LDS_FULL_JSON(object)]);
       await object.save();
       // Check if localDatastore updated localId to objectId
       localDatastore = await Parse.LocalDatastore._getAllContents();
@@ -84,6 +90,55 @@ function runTest(controller) {
       assert.deepEqual(localDatastore[LDS_KEY(object)], [object._toFullJSON()]);
     });
 
+    it(`${controller.name} can store data to pin (unsaved)`, async () => {
+      const object = new TestObject();
+      object.set('foo', 'bar');
+      await object.pin();
+
+      const query = new Parse.Query(TestObject);
+      query.fromLocalDatastore();
+      let results = await query.find();
+      assert.equal(results.length, 1);
+
+      let pinnedObject = results[0];
+      assert.equal(pinnedObject.get('foo'), 'bar');
+      pinnedObject.set('foo', 'baz');
+      await pinnedObject.pin();
+
+      results = await query.find();
+      assert.equal(results.length, 1);
+      pinnedObject = results[0];
+      assert.equal(pinnedObject.get('foo'), 'baz');
+    });
+
+    it(`${controller.name} can query unsaved pin and save`, async () => {
+      const object = new TestObject();
+      object.set('foo', 'bar');
+      await object.pin();
+
+      const query = new Parse.Query(TestObject);
+      query.fromLocalDatastore();
+      let results = await query.find();
+
+      assert.equal(results.length, 1);
+
+      let pinnedObject = results[0];
+      assert.equal(pinnedObject.get('foo'), 'bar');
+
+      pinnedObject.set('foo', 'baz');
+      await pinnedObject.save();
+
+      assert(pinnedObject.id);
+      assert.equal(pinnedObject._localId, undefined);
+
+      results = await query.find();
+      pinnedObject = results[0];
+
+      assert.equal(pinnedObject.get('foo'), 'baz');
+      assert(pinnedObject.id);
+      assert.equal(pinnedObject._localId, undefined);
+    });
+
     it(`${controller.name} cannot pin unsaved pointer`, async () => {
       try {
         const object = new TestObject();
@@ -174,9 +229,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -216,9 +271,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_pin'], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -315,17 +370,17 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await obj2.unPin();
 
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 3);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -347,9 +402,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -358,8 +413,8 @@ function runTest(controller) {
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 3);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
     });
 
     it(`${controller.name} can unPin / unPinAll without pin (unsaved)`, async () => {
@@ -407,16 +462,16 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert(Object.keys(localDatastore).length === 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.unPinAll([obj1, obj2]);
 
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 2);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -437,9 +492,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -461,17 +516,17 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert(Object.keys(localDatastore).length === 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.unPinAllObjects();
 
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 3);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -493,9 +548,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[DEFAULT_PIN], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -518,16 +573,16 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert(Object.keys(localDatastore).length === 4);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_unpin'], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.unPinAllWithName('test_unpin', [obj1, obj2]);
 
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 2);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_unpin'], [LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -548,9 +603,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_unpin'], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -572,17 +627,17 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert(Object.keys(localDatastore).length === 4);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_unpin'], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.unPinAllObjectsWithName('test_unpin');
 
       localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 3);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -604,9 +659,9 @@ function runTest(controller) {
       let localDatastore = await Parse.LocalDatastore._getAllContents();
       assert.equal(Object.keys(localDatastore).length, 4);
       assert.deepEqual(localDatastore[PIN_PREFIX + 'test_unpin'], [LDS_KEY(obj1), LDS_KEY(obj2), LDS_KEY(obj3)]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [obj1._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [obj2._toFullJSON()]);
-      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [obj3._toFullJSON()]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj1)], [LDS_FULL_JSON(obj1)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj2)], [LDS_FULL_JSON(obj2)]);
+      assert.deepEqual(localDatastore[LDS_KEY(obj3)], [LDS_FULL_JSON(obj3)]);
 
       await Parse.Object.saveAll(objects);
 
@@ -871,6 +926,25 @@ function runTest(controller) {
       const childJSON = updatedLDS[LDS_KEY(child)];
       assert.equal(childJSON.foo, 'changed');
     });
+
+    it(`${controller.name} can update Local Datastore from network ignore unsaved`, async () => {
+      const object = new TestObject();
+      const item = new Item();
+      await item.save();
+      await Parse.Object.pinAll([object, item]);
+
+      // Updates item with { foo: 'changed' }
+      const params = { id: item.id };
+      await Parse.Cloud.run('TestFetchFromLocalDatastore', params);
+
+      Parse.LocalDatastore.isSyncing = false;
+
+      await Parse.LocalDatastore.updateFromServer();
+
+      const updatedLDS = await Parse.LocalDatastore._getAllContents();
+      const itemJSON = updatedLDS[LDS_KEY(item)];
+      assert.equal(itemJSON.foo, 'changed');
+    });
   });
 
   describe(`Parse Query Pinning (${controller.name})`, () => {
diff --git a/src/__tests__/LocalDatastore-test.js b/src/__tests__/LocalDatastore-test.js
index 6c326ef..738565f 100644
--- a/src/__tests__/LocalDatastore-test.js
+++ b/src/__tests__/LocalDatastore-test.js
@@ -66,7 +66,7 @@ class MockObject {
   }
 
   _getId() {
-    return this.id;
+    return this.id || this._localId;
   }
 
   set(key, value) {
@@ -161,6 +161,17 @@ describe('LocalDatastore', () => {
     expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(2);
   });
 
+  it('_handlePinAllWithName with localId', async () => {
+    const object = new ParseObject('Item');
+    object._localId = 'local0';
+    object.id = null;
+    await LocalDatastore._handlePinAllWithName('test_pin', [object]);
+    expect(mockLocalStorageController.pinWithName.mock.calls[0][0]).toEqual('Parse_LDS_Item_local0');
+    expect(mockLocalStorageController.pinWithName.mock.calls[0][1]).toEqual([
+      { __type: 'Object', className: 'Item', _localId: 'local0' }
+    ]);
+  });
+
   it('_handlePinAllWithName default pin', async () => {
     const object = new ParseObject('Item');
     await LocalDatastore._handlePinAllWithName(DEFAULT_PIN, [object]);
@@ -636,6 +647,40 @@ describe('LocalDatastore', () => {
     expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(1);
   });
 
+  it('updateFromServer ignore unsaved objects', async () => {
+    LocalDatastore.isEnabled = true;
+    LocalDatastore.isSyncing = false;
+
+    const object = new ParseObject('Item');
+    object._localId = 'local0';
+    object.id = null;
+
+    const OBJECT_KEY = LocalDatastore.getKeyForObject(object);
+    const LDS = {
+      [OBJECT_KEY]: [object._toFullJSON()],
+      [KEY1]: [item1._toFullJSON()],
+      [`${PIN_PREFIX}_testPinName`]: [KEY1, OBJECT_KEY],
+      [DEFAULT_PIN]: [KEY1, OBJECT_KEY],
+    };
+
+    mockLocalStorageController
+      .getAllContents
+      .mockImplementationOnce(() => LDS);
+
+    item1.set('updatedField', 'foo');
+    mockQueryFind.mockImplementationOnce(() => Promise.resolve([item1]));
+
+    await LocalDatastore.updateFromServer();
+
+    expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
+    expect(ParseQuery).toHaveBeenCalledTimes(1);
+    const mockQueryInstance = ParseQuery.mock.instances[0];
+
+    expect(mockQueryInstance.equalTo.mock.calls.length).toBe(1);
+    expect(mockQueryFind).toHaveBeenCalledTimes(1);
+    expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(1);
+  });
+
   it('updateFromServer handle error', async () => {
     LocalDatastore.isEnabled = true;
     LocalDatastore.isSyncing = false;
diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js
index 6c62346..58d4c71 100644
--- a/src/__tests__/ParseQuery-test.js
+++ b/src/__tests__/ParseQuery-test.js
@@ -2268,6 +2268,38 @@ describe('ParseQuery LocalDatastore', () => {
     expect(results[0].id).toEqual(obj1.objectId);
   });
 
+  it('can query offline with localId', async () => {
+    const obj1 = {
+      className: 'Item',
+      _localId: 'local0',
+      count: 2,
+    };
+
+    const obj2 = {
+      className: 'Item',
+      objectId: 'objectId2',
+    };
+
+    const obj3 = {
+      className: 'Unknown',
+      objectId: 'objectId3',
+    };
+
+    mockLocalDatastore
+      ._serializeObjectsFromPinName
+      .mockImplementationOnce(() => [obj1, obj2, obj3]);
+
+    mockLocalDatastore
+      .checkIfEnabled
+      .mockImplementationOnce(() => true);
+
+    const q = new ParseQuery('Item');
+    q.equalTo('count', 2);
+    q.fromLocalDatastore();
+    const results = await q.find();
+    expect(results[0]._localId).toEqual(obj1._localId);
+  });
+
   it('can query offline first', async () => {
     const obj1 = {
       className: 'Item',