Repository: parse-community/Parse-SDK-JS
Pull request #1303 (2021-02-23 11:46:39)

Allow nested increment on undefined fields
Calling increment on a field that doesn't exists (serverData stored in object state) gets ignored.
```
const obj = new TestObject();
obj.increment('a.b.c.d'); // ignored
await obj.save();
```
I believe this was ignored because the server doesn't support it and throws the following error
```
Unhandled promise rejection: TypeError: Cannot read property 'split' of undefined
```


peril-parse-community[bot]: 
<!--
  0 failure: 
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Peril;
-->



Danger run resulted in 1 warning; to find out more, see the [checks page](https://github.com/parse-community/Parse-SDK-JS/runs/1960841044).
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a>
</p>

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1303?src=pr&el=h1) Report
> Merging [#1303](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1303?src=pr&el=desc) (d83e8e8) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/1abb8415a885be7c9e968526c1533097a3c83886?el=desc) (1abb841) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@            Coverage Diff            @@
##            master     #1303   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           60        60           
  Lines         5837      5832    -5     
  Branches      1312      1310    -2     
=========================================
- Hits          5837      5832    -5     
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1303?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseObject.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1303/diff?src=pr&el=tree#diff-c3JjL1BhcnNlT2JqZWN0Lmpz) | `100.00% <ø> (ø)` | |

------

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

mtrezza: What is the change? Is a new field with this PR incremented when saving?
dplewis: The change is that we are allowing this request to go to the server.
mtrezza: Do we have enough integration test in this repo to ensure letting this go to the server has no detrimental effects? 

Patch:
diff --git a/src/EventuallyQueue.js b/src/EventuallyQueue.js
index b84fe9c..8925fda 100644
--- a/src/EventuallyQueue.js
+++ b/src/EventuallyQueue.js
@@ -308,12 +308,14 @@ const EventuallyQueue = {
     }
     polling = setInterval(() => {
       const RESTController = CoreManager.getRESTController();
-      RESTController.ajax('GET', CoreManager.get('SERVER_URL')).catch(error => {
-        if (error !== 'Unable to connect to the Parse API') {
-          this.stopPoll();
-          return this.sendQueue();
-        }
-      });
+      RESTController.request('GET', 'health')
+        .then(({ status }) => {
+          if (status === 'ok') {
+            this.stopPoll();
+            return this.sendQueue();
+          }
+        })
+        .catch(e => e);
     }, ms);
   },
 
diff --git a/src/Parse.js b/src/Parse.js
index 41f9c87..3d8af96 100644
--- a/src/Parse.js
+++ b/src/Parse.js
@@ -80,6 +80,16 @@ const Parse = {
     CoreManager.setLocalDatastoreController(controller);
   },
 
+  /**
+   * Returns information regarding the current server's health
+   *
+   * @returns {Promise}
+   * @static
+   */
+  getServerHealth() {
+    return CoreManager.getRESTController().request('GET', 'health');
+  },
+
   /**
    * @member {string} Parse.applicationId
    * @static
diff --git a/src/ParseObject.js b/src/ParseObject.js
index 6ca88db..2885c8e 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -741,15 +741,6 @@ class ParseObject {
 
     const currentAttributes = this.attributes;
 
-    // Only set nested fields if exists
-    const serverData = this._getServerData();
-    if (typeof key === 'string' && key.includes('.')) {
-      const field = key.split('.')[0];
-      if (!serverData[field]) {
-        return this;
-      }
-    }
-
     // Calculate new values
     const newValues = {};
     for (const attr in newOps) {
diff --git a/src/ParseQuery.js b/src/ParseQuery.js
index 4f3c358..57228e5 100644
--- a/src/ParseQuery.js
+++ b/src/ParseQuery.js
@@ -1573,13 +1573,14 @@ class ParseQuery {
    *
    * @param {string} key The key that the string to match is stored in.
    * @param {string} prefix The substring that the value must start with.
+   * @param {string} modifiers The regular expression mode.
    * @returns {Parse.Query} Returns the query, so you can chain this call.
    */
-  startsWith(key: string, prefix: string): ParseQuery {
+  startsWith(key: string, prefix: string, modifiers: string): ParseQuery {
     if (typeof prefix !== 'string') {
       throw new Error('The value being searched for must be a string.');
     }
-    return this._addCondition(key, '$regex', this._regexStartWith(prefix));
+    return this.matches(key, this._regexStartWith(prefix), modifiers);
   }
 
   /**
@@ -1588,13 +1589,14 @@ class ParseQuery {
    *
    * @param {string} key The key that the string to match is stored in.
    * @param {string} suffix The substring that the value must end with.
+   * @param {string} modifiers The regular expression mode.
    * @returns {Parse.Query} Returns the query, so you can chain this call.
    */
-  endsWith(key: string, suffix: string): ParseQuery {
+  endsWith(key: string, suffix: string, modifiers: string): ParseQuery {
     if (typeof suffix !== 'string') {
       throw new Error('The value being searched for must be a string.');
     }
-    return this._addCondition(key, '$regex', quote(suffix) + '$');
+    return this.matches(key, quote(suffix) + '$', modifiers);
   }
 
   /**


Test patch:
diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index 45304df..39992c1 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -414,16 +414,6 @@ describe('Parse Object', () => {
     assert.equal(result.get('objectField').unknown, 20);
   });
 
-  it('ignore set nested fields on new object', async () => {
-    const obj = new TestObject();
-    obj.set('objectField.number', 5);
-    assert.deepEqual(obj._getPendingOps()[0], {});
-    assert.equal(obj.get('objectField'), undefined);
-
-    await obj.save();
-    assert.equal(obj.get('objectField'), undefined);
-  });
-
   it('can set nested fields two levels', async () => {
     const obj = new TestObject({ objectField: { foo: { bar: 5 } } });
     assert.equal(obj.get('objectField').foo.bar, 5);
diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js
index 4815585..90f8292 100644
--- a/integration/test/ParseQueryTest.js
+++ b/integration/test/ParseQueryTest.js
@@ -1158,7 +1158,7 @@ describe('Parse Query', () => {
       });
   });
 
-  it('can test case insensitive regex', done => {
+  it('can test case insensitive matches', done => {
     const obj = new TestObject();
     obj.set('myString', 'hockey');
     obj
@@ -1175,6 +1175,32 @@ describe('Parse Query', () => {
       });
   });
 
+  it('can test case insensitive startsWith', async () => {
+    const obj = new TestObject();
+    obj.set('myString', 'basketball');
+    await obj.save();
+
+    const query = new Parse.Query(TestObject);
+    query.startsWith('myString', 'baSKet', 'i');
+    const results = await query.find();
+
+    assert.strictEqual(results.length, 1);
+    assert.strictEqual(results[0].get('myString'), 'basketball');
+  });
+
+  it('can test case insensitive endsWith', async () => {
+    const obj = new TestObject();
+    obj.set('myString', 'basketball');
+    await obj.save();
+
+    const query = new Parse.Query(TestObject);
+    query.endsWith('myString', 'tBAll', 'i');
+    const results = await query.find();
+
+    assert.strictEqual(results.length, 1);
+    assert.strictEqual(results[0].get('myString'), 'basketball');
+  });
+
   it('fails for invalid regex options', done => {
     const query = new Parse.Query(TestObject);
     query.matches('myString', 'football', 'some invalid thing');
diff --git a/src/__tests__/EventuallyQueue-test.js b/src/__tests__/EventuallyQueue-test.js
index c5460d2..d514367 100644
--- a/src/__tests__/EventuallyQueue-test.js
+++ b/src/__tests__/EventuallyQueue-test.js
@@ -52,6 +52,12 @@ const RESTController = require('../RESTController');
 const Storage = require('../Storage');
 const mockXHR = require('./test_helpers/mockXHR');
 
+CoreManager.setInstallationController({
+  currentInstallationId() {
+    return Promise.resolve('iid');
+  },
+});
+
 function flushPromises() {
   return new Promise(resolve => setImmediate(resolve));
 }
@@ -400,7 +406,7 @@ describe('EventuallyQueue', () => {
 
   it('can poll server', async () => {
     jest.spyOn(EventuallyQueue, 'sendQueue').mockImplementationOnce(() => {});
-    RESTController._setXHR(mockXHR([{ status: 107, response: { error: 'ok' } }]));
+    RESTController._setXHR(mockXHR([{ status: 200, response: { status: 'ok' } }]));
     EventuallyQueue.poll();
     expect(EventuallyQueue.isPolling()).toBe(true);
     jest.runOnlyPendingTimers();
diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js
index 9e6d548..afe8ebd 100644
--- a/src/__tests__/Parse-test.js
+++ b/src/__tests__/Parse-test.js
@@ -161,6 +161,18 @@ describe('Parse module', () => {
     CoreManager.set('REQUEST_BATCH_SIZE', 20);
   });
 
+  it('getServerHealth', () => {
+    const controller = {
+      request: jest.fn(),
+      ajax: jest.fn(),
+    };
+    CoreManager.setRESTController(controller);
+    Parse.getServerHealth();
+    const [method, path] = controller.request.mock.calls[0];
+    expect(method).toBe('GET');
+    expect(path).toBe('health');
+  });
+
   it('_request', () => {
     const controller = {
       request: jest.fn(),
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 2dbdf35..62b6a25 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -699,16 +699,6 @@ describe('ParseObject', () => {
     expect(o.get('objectField').letter).toEqual('a');
   });
 
-  it('ignore set nested field on new object', () => {
-    const o = new ParseObject('Person');
-    o.set('objectField.number', 20);
-
-    expect(o.attributes).toEqual({});
-    expect(o.op('objectField.number') instanceof SetOp).toBe(false);
-    expect(o.dirtyKeys()).toEqual([]);
-    expect(o._getSaveJSON()).toEqual({});
-  });
-
   it('can add elements to an array field', () => {
     const o = new ParseObject('Schedule');
     o.add('available', 'Monday');