Repository: parse-community/Parse-SDK-JS
Pull request #461 (2017-07-09 08:50:45)

Support React Native < 0.43 once again

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/461?src=pr&el=h1) Report
> Merging [#461](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/461?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/13438fe6ea90ffbeb4c7b78c1419827ad152141e?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `66.66%`.

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

```diff
@@            Coverage Diff             @@
##           master     #461      +/-   ##
==========================================
- Coverage   85.39%   85.37%   -0.02%     
==========================================
  Files          46       46              
  Lines        3724     3727       +3     
  Branches      854      854              
==========================================
+ Hits         3180     3182       +2     
- Misses        544      545       +1
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/461?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/StorageController.react-native.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/461?src=pr&el=tree#diff-c3JjL1N0b3JhZ2VDb250cm9sbGVyLnJlYWN0LW5hdGl2ZS5qcw==) | `83.33% <66.66%> (-2.39%)` | :arrow_down: |

------

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

flovilmart: @arniu alright, is it working for you correctly?

Patch:
diff --git a/README.md b/README.md
index f8a513d..d8e8fc3 100644
--- a/README.md
+++ b/README.md
@@ -36,8 +36,6 @@ For React Native applications, include `'parse/react-native'`:
 var Parse = require('parse/react-native');
 ```
 
-> As of `v1.10`, Parse-SDK-JS supports React Native 0.43+. Please use `v1.9.2` for previous versions of React Native.
-
 ## License
 
 ```
diff --git a/integration/package.json b/integration/package.json
index a6b6bca..b5a35b9 100644
--- a/integration/package.json
+++ b/integration/package.json
@@ -3,7 +3,7 @@
   "dependencies": {
     "express": "^4.13.4",
     "mocha": "^2.4.5",
-    "parse-server": "^2.4.2"
+    "parse-server": "^2.6.0"
   },
   "scripts": {
     "test": "mocha --reporter dot -t 5000"
diff --git a/src/Parse.js b/src/Parse.js
index fc22006..872f4ae 100644
--- a/src/Parse.js
+++ b/src/Parse.js
@@ -99,6 +99,7 @@ Parse.Error = require('./ParseError').default;
 Parse.FacebookUtils = require('./FacebookUtils').default;
 Parse.File = require('./ParseFile').default;
 Parse.GeoPoint = require('./ParseGeoPoint').default;
+Parse.Polygon = require('./ParsePolygon').default;
 Parse.Installation = require('./ParseInstallation').default;
 Parse.Object = require('./ParseObject').default;
 Parse.Op = {
diff --git a/src/ParseObject.js b/src/ParseObject.js
index e5dd6ba..9d82a60 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -735,6 +735,17 @@ export default class ParseObject {
     return this.set(attr, new AddOp([item]));
   }
 
+  /**
+   * Atomically add the objects to the end of the array associated with a given
+   * key.
+   * @method addAll
+   * @param attr {String} The key.
+   * @param items {[]} The items to add.
+   */
+  addAll(attr: string, items: Array<mixed>): ParseObject | boolean {
+    return this.set(attr, new AddOp(items));
+  }
+
   /**
    * Atomically add an object to the array associated with a given key, only
    * if it is not already present in the array. The position of the insert is
@@ -748,6 +759,19 @@ export default class ParseObject {
     return this.set(attr, new AddUniqueOp([item]));
   }
 
+  /**
+   * Atomically add the objects to the array associated with a given key, only
+   * if it is not already present in the array. The position of the insert is
+   * not guaranteed.
+   *
+   * @method addAllUnique
+   * @param attr {String} The key.
+   * @param items {[]} The objects to add.
+   */
+  addAllUnique(attr: string, items: Array<mixed>): ParseObject | boolean {
+    return this.set(attr, new AddUniqueOp(items));
+  }
+
   /**
    * Atomically remove all instances of an object from the array associated
    * with a given key.
@@ -760,6 +784,18 @@ export default class ParseObject {
     return this.set(attr, new RemoveOp([item]));
   }
 
+  /**
+   * Atomically remove all instances of the objects from the array associated
+   * with a given key.
+   *
+   * @method removeAll
+   * @param attr {String} The key.
+   * @param items {[]} The object to remove.
+   */
+  removeAll(attr: string, items: Array<mixed>): ParseObject | boolean {
+    return this.set(attr, new RemoveOp(items));
+  }
+
   /**
    * Returns an instance of a subclass of Parse.Op describing what kind of
    * modification has been performed on this field since the last time it was
diff --git a/src/ParsePolygon.js b/src/ParsePolygon.js
new file mode 100644
index 0000000..0eed04c
--- /dev/null
+++ b/src/ParsePolygon.js
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @flow
+ */
+
+import ParseGeoPoint from './ParseGeoPoint';
+
+/**
+ * Creates a new Polygon with any of the following forms:<br>
+ *   <pre>
+ *   new Polygon([[0,0],[0,1],[1,1],[1,0]])
+ *   new Polygon([GeoPoint, GeoPoint, GeoPoint])
+ *   </pre>
+ * @class Parse.GeoPoint
+ * @constructor
+ *
+ * <p>Represents a coordinates that may be associated
+ * with a key in a ParseObject or used as a reference point for geo queries.
+ * This allows proximity-based queries on the key.</p>
+ *
+ * <p>Example:<pre>
+ *   var polygon = new Parse.Polygon([[0,0],[0,1],[1,1],[1,0]]);
+ *   var object = new Parse.Object("PlaceObject");
+ *   object.set("area", polygon);
+ *   object.save();</pre></p>
+ */
+export default class ParsePolygon {
+  _coordinates: Array;
+
+  constructor(
+    arg1: Array,
+  ) {
+    this._coordinates = ParsePolygon._validate(arg1);
+  }
+
+  /**
+   * Coordinates value for this Polygon.
+   * Throws an exception if not valid type.
+   * @property coordinates
+   * @type Array
+   */
+  get coordinates(): Array {
+    return this._coordinates;
+  }
+
+  set coordinates(coords: Array) {
+    this._coordinates = ParsePolygon._validate(coords);
+  }
+
+  /**
+   * Returns a JSON representation of the GeoPoint, suitable for Parse.
+   * @method toJSON
+   * @return {Object}
+   */
+  toJSON(): { __type: string; coordinates: Array;} {
+    ParsePolygon._validate(this._coordinates);
+    return {
+      __type: 'Polygon',
+      coordinates: this._coordinates,
+    };
+  }
+
+  equals(other: mixed): boolean {
+    if (!(other instanceof ParsePolygon) || (this.coordinates.length !== other.coordinates.length)) {
+      return false;
+    }
+    let isEqual = true;
+
+    for (let i = 1; i < this._coordinates.length; i += 1) {
+      if (this._coordinates[i][0] != other.coordinates[i][0] ||
+          this._coordinates[i][1] != other.coordinates[i][1]) {
+        isEqual = false;
+        break;
+      }
+    }
+    return isEqual;
+  }
+
+  containsPoint(point: ParseGeoPoint): boolean {
+    let minX = this._coordinates[0][0];
+    let maxX = this._coordinates[0][0];
+    let minY = this._coordinates[0][1];
+    let maxY = this._coordinates[0][1];
+
+    for (let i = 1; i < this._coordinates.length; i += 1) {
+        const p = this._coordinates[i];
+        minX = Math.min(p[0], minX);
+        maxX = Math.max(p[0], maxX);
+        minY = Math.min(p[1], minY);
+        maxY = Math.max(p[1], maxY);
+    }
+
+    const outside = (point.latitude < minX || point.latitude > maxX || point.longitude < minY || point.longitude > maxY);
+    if (outside) {
+        return false;
+    }
+
+    let inside = false;
+    for (let i = 0, j = this._coordinates.length - 1 ; i < this._coordinates.length; j = i++) {
+      let startX = this._coordinates[i][0];
+      let startY = this._coordinates[i][1];
+      let endX = this._coordinates[j][0];
+      let endY = this._coordinates[j][1];
+
+      const intersect = (( startY > point.longitude ) != ( endY > point.longitude ) &&
+          point.latitude < ( endX - startX ) * ( point.longitude - startY ) / ( endY - startY ) + startX);
+
+      if (intersect) {
+          inside = !inside;
+      }
+    }
+    return inside;
+  }
+
+  /**
+   * Throws an exception if the given lat-long is out of bounds.
+   * @return {Array}
+   */
+  static _validate(coords: Array) {
+    if (!Array.isArray(coords)) {
+      throw new TypeError('Coordinates must be an Array');
+    }
+    if (coords.length < 3) {
+      throw new TypeError('Polygon must have at least 3 GeoPoints or Points');
+    }
+    const points = [];
+    for (let i = 0; i < coords.length; i += 1) {
+      const coord = coords[i];
+      let geoPoint;
+      if (coord instanceof ParseGeoPoint) {
+        geoPoint = coord;
+      } else if (Array.isArray(coord) && coord.length === 2) {
+        geoPoint = new ParseGeoPoint(coord[0], coord[1]);
+      } else {
+        throw new TypeError('Coordinates must be an Array of GeoPoints or Points');
+      }
+      points.push([geoPoint.latitude, geoPoint.longitude]);
+    }
+    return points;
+  }
+}
diff --git a/src/ParseQuery.js b/src/ParseQuery.js
index 1591869..796856c 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.js
@@ -13,6 +13,7 @@ import CoreManager from './CoreManager';
 import encode from './encode';
 import ParseError from './ParseError';
 import ParseGeoPoint from './ParseGeoPoint';
+import ParsePolygon from './ParsePolygon';
 import ParseObject from './ParseObject';
 import ParsePromise from './ParsePromise';
 
@@ -66,16 +67,19 @@ function handleSelectResult(data: any, select: Array<string>){
 
       pathComponents.forEach((component, index, arr) => {
         // add keys if the expected data is missing
-        if (!obj[component]) {
-          obj[component] = (index == arr.length-1) ? undefined : {};
+        if (obj && !obj.hasOwnProperty(component)) {
+          obj[component] = undefined;
+        }
+        if (obj !== undefined) {
+          obj = obj[component];
         }
-        obj = obj[component];
 
         //add this path component to the server mask so we can fill it in later if needed
         if (index < arr.length-1) {
           if (!serverMask[component]) {
             serverMask[component] = {};
           }
+          serverMask = serverMask[component];
         }
       });
     }
@@ -99,8 +103,10 @@ function handleSelectResult(data: any, select: Array<string>){
         }
       }
       for (var key in mask) {
-        //traverse into objects as needed
-        copyMissingDataWithMask(src[key], dest[key], mask[key], true);
+        if (dest[key] !== undefined && dest[key] !== null && src !== undefined && src !== null) {
+          //traverse into objects as needed
+          copyMissingDataWithMask(src[key], dest[key], mask[key], true);
+        }
       }
     }
 
@@ -1013,6 +1019,19 @@ export default class ParseQuery {
     return this._addCondition(key, '$geoWithin', { '$polygon': points });
   }
 
+  /**
+   * Add a constraint to the query that requires a particular key's
+   * coordinates that contains a ParseGeoPoint
+   *
+   * @method polygonContains
+   * @param {String} key The key to be constrained.
+   * @param {Parse.GeoPoint} GeoPoint
+   * @return {Parse.Query} Returns the query, so you can chain this call.
+   */
+  polygonContains(key: string, point: ParseGeoPoint): ParseQuery {
+    return this._addCondition(key, '$geoIntersects', { '$point': point });
+  }
+
   /** Query Orderings **/
 
   /**
diff --git a/src/StorageController.react-native.js b/src/StorageController.react-native.js
index e13de1a..7c2c6a1 100644
--- a/src/StorageController.react-native.js
+++ b/src/StorageController.react-native.js
@@ -10,8 +10,15 @@
  */
 
 import ParsePromise from './ParsePromise';
+
 // RN packager nonsense
-import { AsyncStorage } from 'react-native/Libraries/react-native/react-native-implementation';
+let AsyncStorage;
+try {
+  // for React Native 0.43+
+  AsyncStorage = require('react-native/Libraries/react-native/react-native-implementation').AsyncStorage;
+} catch (error) {
+  AsyncStorage = require('react-native/Libraries/react-native/react-native.js').AsyncStorage;
+}
 
 var StorageController = {
   async: 1,
diff --git a/src/decode.js b/src/decode.js
index bbc54bd..efc4524 100644
--- a/src/decode.js
+++ b/src/decode.js
@@ -12,6 +12,7 @@
 import ParseACL from './ParseACL';
 import ParseFile from './ParseFile';
 import ParseGeoPoint from './ParseGeoPoint';
+import ParsePolygon from './ParsePolygon';
 import ParseObject from './ParseObject';
 import { opFromJSON } from './ParseOp';
 import ParseRelation from './ParseRelation';
@@ -54,6 +55,9 @@ export default function decode(value: any): any {
       longitude: value.longitude
     });
   }
+  if (value.__type === 'Polygon') {
+    return new ParsePolygon(value.coordinates);
+  }
   var copy = {};
   for (var k in value) {
     copy[k] = decode(value[k]);
diff --git a/src/encode.js b/src/encode.js
index 3652a8c..30d4f11 100644
--- a/src/encode.js
+++ b/src/encode.js
@@ -12,6 +12,7 @@
 import ParseACL from './ParseACL';
 import ParseFile from './ParseFile';
 import ParseGeoPoint from './ParseGeoPoint';
+import ParsePolygon from './ParsePolygon';
 import ParseObject from './ParseObject';
 import { Op } from './ParseOp';
 import ParseRelation from './ParseRelation';
@@ -38,6 +39,7 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean,
   if (value instanceof Op ||
       value instanceof ParseACL ||
       value instanceof ParseGeoPoint ||
+      value instanceof ParsePolygon ||
       value instanceof ParseRelation) {
     return value.toJSON();
   }


Test patch:
diff --git a/integration/test/ParseGeoPointTest.js b/integration/test/ParseGeoPointTest.js
index add6d20..245ba27 100644
--- a/integration/test/ParseGeoPointTest.js
+++ b/integration/test/ParseGeoPointTest.js
@@ -291,7 +291,7 @@ describe('Geo Point', () => {
     const query = new Parse.Query(TestPoint);
     query.withinPolygon('location', []);
     query.find().then(done.fail, (err) => {
-      assert.equal(err.code, Parse.Error.INTERNAL_SERVER_ERROR);
+      assert.equal(err.code, Parse.Error.INVALID_JSON);
       done();
     })
     .fail(done.fail);
diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index c02fda4..3ee6999 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -826,6 +826,24 @@ describe('Parse Object', () => {
     });
   });
 
+  it('can add objects to an array in batch mode', (done) => {
+    let child1 = new Parse.Object('Person');
+    let child2 = new Parse.Object('Person');
+    let parent = new Parse.Object('Person');
+
+    Promise.all([child1.save(), child2.save()]).then((children) => {
+      parent.addAll('children', children);
+      return parent.save();
+    }).then(() => {
+      let query = new Parse.Query('Person');
+      return query.get(parent.id);
+    }).then((p) => {
+      assert.equal(p.get('children')[0].id, child1.id);
+      assert.equal(p.get('children')[1].id, child2.id);
+      done();
+    });
+  });
+
   it('can convert saved objects to json', (done) => {
     let object = new TestObject();
     object.save({ foo: 'bar' }).then(() => {
@@ -865,6 +883,29 @@ describe('Parse Object', () => {
     });
   });
 
+  it('can remove objects from array fields in batch mode', (done) => {
+    let obj1 = new TestObject();
+    let obj2 = new TestObject();
+
+    Promise.all([obj1.save(), obj2.save()]).then((objects) => {
+      let container = new TestObject();
+      container.addAll('array', objects);
+      assert.equal(container.get('array').length, 2);
+      return container.save();
+    }).then((container) => {
+      let o1 = new TestObject();
+      o1.id = obj1.id;
+      let o2 = new TestObject();
+      o2.id = obj2.id;
+      let o3 = new TestObject();
+      o3.id = 'there_is_no_such_object'
+
+      container.removeAll('array', [o1, o2, o3]);
+      assert.equal(container.get('array').length, 0);
+      done();
+    });
+  });
+
   it('can perform async methods', (done) => {
     let object = new TestObject();
     object.set('time', 'adventure');
diff --git a/integration/test/ParsePolygonTest.js b/integration/test/ParsePolygonTest.js
new file mode 100644
index 0000000..7d8c696
--- /dev/null
+++ b/integration/test/ParsePolygonTest.js
@@ -0,0 +1,160 @@
+const assert = require('assert');
+const clear = require('./clear');
+const Parse = require('../../node');
+
+const TestObject = Parse.Object.extend('TestObject');
+
+describe('Polygon', () => {
+  before(() => {
+    Parse.initialize('integration');
+    Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
+    Parse.Storage._clear();
+  });
+
+  beforeEach((done) => {
+    clear().then(() => {
+      done();
+    });
+  });
+
+  it('can save polygon with points', (done) => {
+    const openPoints = [[0,0], [0,1], [1,1], [1,0]];
+    const closedPoints = [[0,0], [0,1], [1,1], [1,0], [0,0]];
+    const polygon = new Parse.Polygon(openPoints);
+    const obj = new TestObject({ polygon });
+    obj.save().then(() => {
+      const query = new Parse.Query(TestObject);
+      query.equalTo('polygon', polygon);
+      return query.find();
+    }).then((results) => {
+      assert.equal(results.length, 1);
+      assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
+      const closedPolygon = new Parse.Polygon(closedPoints);
+      const query = new Parse.Query(TestObject);
+      query.equalTo('polygon', closedPolygon);
+      return query.find();
+    }).then((results) => {
+      assert.equal(results.length, 1);
+      assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
+      done();
+    }, done.fail);
+  });
+
+  it('can save polygon with GeoPoints', (done) => {
+    const p1 = new Parse.GeoPoint(0, 0);
+    const p2 = new Parse.GeoPoint(0, 1);
+    const p3 = new Parse.GeoPoint(1, 1);
+    const p4 = new Parse.GeoPoint(1, 0);
+    const p5 = new Parse.GeoPoint(0, 0);
+    const closedPoints = [[0,0], [0,1], [1,1], [1,0], [0,0]];
+    const polygon = new Parse.Polygon([p1, p2, p3, p4, p5]);
+    const obj = new TestObject({ polygon });
+    obj.save().then(() => {
+      const query = new Parse.Query(TestObject);
+      query.equalTo('polygon', polygon);
+      return query.find();
+    }).then((results) => {
+      assert.equal(results.length, 1);
+      assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
+      const closedPolygon = new Parse.Polygon(closedPoints);
+      const query = new Parse.Query(TestObject);
+      query.equalTo('polygon', closedPolygon);
+      return query.find();
+    }).then((results) => {
+      assert.deepEqual(results[0].get('polygon').coordinates, closedPoints);
+      done();
+    }, done.fail);
+  });
+
+  it('fail save with 3 point minumum', (done) => {
+    try {
+      const polygon = new Parse.Polygon([[0, 0]]);
+    } catch (e) {
+      done();
+    }
+  });
+
+  it('fail save with non array', (done) => {
+    try {
+      const polygon = new Parse.Polygon(123);
+    } catch (e) {
+      done();
+    }
+  });
+
+  it('fail save with invalid array', (done) => {
+    try {
+      const polygon = new Parse.Polygon([['str1'], ['str2'], ['str3']]);
+    } catch (e) {
+      done();
+    }
+  });
+
+  it('containsPoint', (done) => {
+    const points = [[0,0], [0,1], [1,1], [1,0]];
+    const inside = new Parse.GeoPoint(0.5, 0.5);
+    const outside = new Parse.GeoPoint(10, 10);
+    const polygon = new Parse.Polygon(points);
+
+    assert.equal(polygon.containsPoint(inside), true);
+    assert.equal(polygon.containsPoint(outside), false);
+    done();
+  });
+
+  it('equality', (done) => {
+    const points = [[0,0], [0,1], [1,1], [1,0]];
+    const diff = [[0,0], [0,2], [2,2], [2,0]];
+
+    const polygonA = new Parse.Polygon(points);
+    const polygonB = new Parse.Polygon(points);
+    const polygonC = new Parse.Polygon(diff);
+
+    assert.equal(polygonA.equals(polygonA), true);
+    assert.equal(polygonA.equals(polygonB), true);
+    assert.equal(polygonB.equals(polygonA), true);
+
+    assert.equal(polygonA.equals(true), false);
+    assert.equal(polygonA.equals(polygonC), false);
+
+    done();
+  });
+
+  it('supports polygonContains', (done) => {
+    const p1 = [[0,0], [0,1], [1,1], [1,0]];
+    const p2 = [[0,0], [0,2], [2,2], [2,0]];
+    const p3 = [[10,10], [10,15], [15,15], [15,10], [10,10]];
+
+    const polygon1 = new Parse.Polygon(p1);
+    const polygon2 = new Parse.Polygon(p2);
+    const polygon3 = new Parse.Polygon(p3);
+
+    const obj1 = new TestObject({ polygon: polygon1 });
+    const obj2 = new TestObject({ polygon: polygon2 });
+    const obj3 = new TestObject({ polygon: polygon3 });
+
+    Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
+      const point = new Parse.GeoPoint(0.5, 0.5);
+      const query = new Parse.Query(TestObject);
+      query.polygonContains('polygon', point);
+      return query.find();
+    }).then((results) => {
+      assert.equal(results.length, 2);
+      done();
+    }, done.fail);
+  });
+
+  it('polygonContains invalid input', (done) => {
+    const points = [[0,0], [0,1], [1,1], [1,0]];
+    const polygon = new Parse.Polygon(points);
+    const obj = new TestObject({ polygon });
+    obj.save().then(() => {
+      const query = new Parse.Query(TestObject);
+      query.polygonContains('polygon', 1234);
+      return query.find();
+    }).then(() => {
+      fail();
+    }).catch(() => {
+      done();
+    });
+  });
+});
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 17e5395..9432036 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -423,6 +423,25 @@ describe('ParseObject', () => {
     expect(o.get('available')).toEqual(['Monday', 'Wednesday', 'Thursday']);
   });
 
+  it('can add elements to an array field in batch mode', () => {
+    var o = new ParseObject('Schedule');
+    o.addAll('available', ['Monday', 'Wednesday']);
+    expect(o.get('available')).toEqual(['Monday', 'Wednesday']);
+
+    o.set('colors', ['red']);
+    o.addAll('colors', ['green', 'blue']);
+    expect(o.get('colors')).toEqual(['red', 'green', 'blue']);
+
+    o._handleSaveResponse({
+      objectId: 'S1',
+      available: ['Monday', 'Wednesday'],
+      colors: ['red', 'green', 'blue']
+    });
+
+    o.addAllUnique('available', ['Thursday', 'Monday']);
+    expect(o.get('available').length).toEqual(3);
+  });
+
   it('can remove elements from an array field', () => {
     var o = new ParseObject('Schedule');
     o.set('available', ['Monday', 'Tuesday']);
@@ -440,6 +459,21 @@ describe('ParseObject', () => {
     expect(o.get('available')).toEqual([]);
   });
 
+  it('can remove elements from an array field in batch mode', () => {
+    var o = new ParseObject('Schedule');
+    o.set('available', ['Monday', 'Tuesday']);
+    o.removeAll('available', ['Tuesday', 'Saturday']);
+    expect(o.get('available')).toEqual(['Monday']);
+
+    o._handleSaveResponse({
+      objectId: 'S2',
+      available: ['Monday']
+    });
+
+    o.removeAll('available', ['Monday', 'Tuesday']);
+    expect(o.get('available')).toEqual([]);
+  });
+
   it('can chain sets', () => {
     var o = new ParseObject('Person');
     o.set('developer', true).set('platform', 'web');
diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js
index 61d3b6c..d1d329b 100644
--- a/src/__tests__/ParseQuery-test.js
+++ b/src/__tests__/ParseQuery-test.js
@@ -1584,4 +1584,105 @@ describe('ParseQuery', () => {
     });
 
   });
+
+  it('selecting sub-objects does not inject objects when sub-object does not exist', (done) => {
+    jest.dontMock("../ParseObject");
+    jest.resetModules();
+    ParseObject = require('../ParseObject').default;
+    CoreManager = require('../CoreManager');
+    ParseQuery = require('../ParseQuery').default;
+
+    ParseObject.enableSingleInstance();
+    
+    var objectToReturn = { 
+      objectId: 'T01', 
+      name: 'Name', 
+      tbd: 'exists', 
+      className:"Thing", 
+      createdAt: '2017-01-10T10:00:00Z'
+    };
+
+    CoreManager.setQueryController({
+      find(className, params, options) {
+        return ParsePromise.as({
+          results: [objectToReturn]
+        });
+      }
+    });
+
+    var q = new ParseQuery("Thing");
+    q.select("other", "tbd", "subObject.key1")
+    var testObject;
+    return q.find().then((results) => {
+      testObject = results[0];
+      
+      expect(testObject.get("name")).toBe("Name");
+      expect(testObject.has("other")).toBe(false);
+      expect(testObject.has("subObject")).toBe(false);
+
+    }).then(() => {
+      done();
+    }, (error) => {
+      done.fail(error);
+    });
+  });
+
+  it('removes missing sub objects from the cached object when they are selected', (done) => {
+    jest.dontMock("../ParseObject");
+    jest.resetModules();
+    ParseObject = require('../ParseObject').default;
+    CoreManager = require('../CoreManager');
+    ParseQuery = require('../ParseQuery').default;
+
+    ParseObject.enableSingleInstance();
+    
+    var objectToReturn = { 
+      objectId: 'T01', 
+      name: 'Name', 
+      tbd: 'exists', 
+      className:"Thing", 
+      subObject1: {foo:"bar"},
+      subObject2: {foo:"bar"},
+      subObject3: {foo:"bar"},
+      subObject5: {subSubObject:{foo:"foo", bar:"bar"}},
+      createdAt: '2017-01-10T10:00:00Z'
+    };
+
+    CoreManager.setQueryController({
+      find(className, params, options) {
+        return ParsePromise.as({
+          results: [objectToReturn]
+        });
+      }
+    });
+
+    var q = new ParseQuery("Thing");
+    var testObject;
+    return q.find().then((results) => {
+      testObject = results[0];
+      
+      expect(testObject.has("subObject1")).toBe(true);
+      expect(testObject.has("subObject2")).toBe(true);
+      expect(testObject.has("subObject3")).toBe(true);
+      expect(testObject.has("subObject4")).toBe(false);
+
+      var q2 = new ParseQuery("Thing");
+      q.select("name","subObject1", "subObject2.foo", "subObject4.foo", "subObject5.subSubObject.foo");
+      objectToReturn = { objectId: 'T01', name:"Name", subObject4: {foo:"bar"}, subObject5: {subSubObject:{}}};
+      return q.find();
+    }).then((results)=>{
+      expect(testObject.has("subObject1")).toBe(false); //selected and not returned
+      expect(testObject.has("subObject2")).toBe(false); //selected and not returned
+      expect(testObject.has("subObject3")).toBe(true); //not selected, so should still be there
+      expect(testObject.has("subObject4")).toBe(true); //selected and just added
+      expect(testObject.has("subObject5")).toBe(true); 
+      expect(testObject.get("subObject5").subSubObject).toBeDefined();
+      expect(testObject.get("subObject5").subSubObject.bar).toBeDefined(); //not selected but a sibiling was, so should still be there
+    }).then(() => {
+      done();
+    }, (error) => {
+      done.fail(error);
+    });
+  });
+
 });
diff --git a/src/__tests__/Storage-test.js b/src/__tests__/Storage-test.js
index 76df4db..d0eecbb 100644
--- a/src/__tests__/Storage-test.js
+++ b/src/__tests__/Storage-test.js
@@ -29,10 +29,15 @@ var mockRNStorageInterface = {
   },
 };
 
+// for React Native 0.43+
 jest.mock('react-native/Libraries/react-native/react-native-implementation', () => {
   return {AsyncStorage: mockRNStorageInterface};
 }, {virtual: true});
 
+jest.mock('react-native/Libraries/react-native/react-native.js', () => {
+  return {AsyncStorage: mockRNStorageInterface};
+}, {virtual: true});
+
 var CoreManager = require('../CoreManager');
 var ParsePromise = require('../ParsePromise').default;