Repository: parse-community/Parse-SDK-JS (js)

Issue:
How to increment a nested object field ?
### New Issue Checklist
<!--
    Please check the following boxes [x] before submitting your issue.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Server!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
- [x] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue).
- [x] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). <!-- We don't investigate issues for outdated releases. -->

### Issue Description
<!-- What is the specific issue with Parse Server? -->
This is a simpler case version of issue #6687. I'm not sure if i'm doing anything wrong, but i think this should work

I'm trying to increment a value of a newly created object. 

Ex:
```js
db.test.findOneAndUpdate(
    {"_id":"20mMYzLTjx"} ,
    {$inc:{ "a.b.c.d" : 1 }}
)
```


### Steps to reproduce
<!-- How can someone else reproduce the issue? -->
```js
const obj= await Parse.Object
    .extend('test')
    .createWithoutData('20mMYzLTjx').fetch()

// obj.get('a') === {}   (my schema defaults a to {}

obj.increment('a.b.c.d' , 1 )
await obj.save()

```


### Actual Outcome
<!-- What outcome, for example query result, did you get? -->
```
TypeError: Cannot read property 'd' of undefined at Object.estimateAttributes (.....parse-server\node_modules\parse\lib\node\ObjectStateMutations.js:234:28)
```

Seems to me that https://github.com/parse-community/Parse-SDK-JS/blob/master/src/ObjectStateMutations.js#L140

```patch
          for (let i = 0; i < fields.length - 1; i++) {
-            object = object[fields[i]];
+            object = object[fields[i]] || {}; 
          }
```


### Expected Outcome
<!-- What outcome, for example query result, did you expect? -->
Path `a.b.c.d` get created and persisted


### Failing Test Case / Pull Request
<!--
    Check one of the following boxes [x] if you added a PR and add the link.
    See the contribution guide for how add a test cases:
    https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md
-->

- [x] 🤩 I submitted a PR with a fix and a test case.
- [ ] 🧐 I submitted a PR with a failing test case.

###  Environment
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->


Server
- Parse Server version: `v4.5.0 /  3d76643286d41b954b6c8e2d6614657c891a31e7`
- Operating system: `Win 10`
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `local` 

Database
- System (MongoDB or Postgres): `Mongo`
- Database version: `4.4.4`
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `remote/self hosted`

Client
- SDK (iOS, Android, JavaScript, PHP, Unity, etc): `Js`
- SDK version: `2.17.0`

### Logs
<!-- Include relevant logs here. Turn on additional logging by configuring VERBOSE=1 in your environment. -->


Pull request:
When traversing fields, ensure we create them as we go to avoid acces…
Fix #1299



Patch:
diff --git a/src/ObjectStateMutations.js b/src/ObjectStateMutations.js
index f991184..bcac048 100644
--- a/src/ObjectStateMutations.js
+++ b/src/ObjectStateMutations.js
@@ -138,7 +138,11 @@ export function estimateAttributes(
           data[first] = { ...serverData[first] };
           let object = { ...data };
           for (let i = 0; i < fields.length - 1; i++) {
-            object = object[fields[i]];
+            const key = fields[i];
+            if (!(key in object)) {
+              object[key] = {};
+            }
+            object = object[key];
           }
           object[last] = pendingOps[i][attr].applyTo(object[last]);
         } else {
diff --git a/src/Push.js b/src/Push.js
index dcfec33..4ef1006 100644
--- a/src/Push.js
+++ b/src/Push.js
@@ -13,6 +13,7 @@ import CoreManager from './CoreManager';
 import ParseQuery from './ParseQuery';
 
 import type { WhereClause } from './ParseQuery';
+import type { FullOptions } from './RESTController';
 
 export type PushData = {
   where?: WhereClause | ParseQuery,
@@ -37,7 +38,7 @@ export type PushData = {
  *
  * @function send
  * @name Parse.Push.send
- * @param {object} data -  The data of the push notification.  Valid fields
+ * @param {object} data -  The data of the push notification. Valid fields
  * are:
  *   <ol>
  *     <li>channels - An Array of channels to push to.</li>
@@ -49,10 +50,15 @@ export type PushData = {
  *         a set of installations to push to.</li>
  *     <li>data - The data to send as part of the push.</li>
  *   <ol>
+ * @param {object} options Valid options
+ * are:<ul>
+ *   <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
+ *     be used for this request.
+ * </ul>
  * @returns {Promise} A promise that is fulfilled when the push request
  *     completes.
  */
-export function send(data: PushData): Promise {
+export function send(data: PushData, options?: FullOptions = {}): Promise {
   if (data.where && data.where instanceof ParseQuery) {
     data.where = data.where.toJSON().where;
   }
@@ -69,14 +75,39 @@ export function send(data: PushData): Promise {
     throw new Error('expiration_time and expiration_interval cannot both be set.');
   }
 
-  return CoreManager.getPushController().send(data);
+  const pushOptions = { useMasterKey: true };
+  if (options.hasOwnProperty('useMasterKey')) {
+    pushOptions.useMasterKey = options.useMasterKey;
+  }
+
+  return CoreManager.getPushController().send(data, pushOptions);
+}
+
+/**
+ * Gets push status by Id
+ *
+ * @function getPushStatus
+ * @name Parse.Push.getPushStatus
+ * @param {string} pushStatusId The Id of Push Status.
+ * @param {object} options Valid options
+ * are:<ul>
+ *   <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
+ *     be used for this request.
+ * </ul>
+ * @returns {Parse.Object} Status of Push.
+ */
+export function getPushStatus(pushStatusId: string, options?: FullOptions = {}): Promise<string> {
+  const pushOptions = { useMasterKey: true };
+  if (options.hasOwnProperty('useMasterKey')) {
+    pushOptions.useMasterKey = options.useMasterKey;
+  }
+  const query = new ParseQuery('_PushStatus');
+  return query.get(pushStatusId, pushOptions);
 }
 
 const DefaultController = {
-  send(data: PushData) {
-    return CoreManager.getRESTController().request('POST', 'push', data, {
-      useMasterKey: true,
-    });
+  send(data: PushData, options?: FullOptions) {
+    return CoreManager.getRESTController().request('POST', 'push', data, options);
   },
 };
 
diff --git a/src/RESTController.js b/src/RESTController.js
index 292cf90..85e18fc 100644
--- a/src/RESTController.js
+++ b/src/RESTController.js
@@ -122,6 +122,9 @@ const RESTController = {
               if ((xhr.getAllResponseHeaders() || '').includes('x-parse-job-status-id: ')) {
                 response = xhr.getResponseHeader('x-parse-job-status-id');
               }
+              if ((xhr.getAllResponseHeaders() || '').includes('x-parse-push-status-id: ')) {
+                response = xhr.getResponseHeader('x-parse-push-status-id');
+              }
             }
           } catch (e) {
             promise.reject(e.toString());


Test patch:
diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index 3c94ad3..45304df 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -278,6 +278,20 @@ describe('Parse Object', () => {
     assert.equal(result.get('objectField').number, 20);
   });
 
+  it('can increment nested four levels', async () => {
+    const obj = new TestObject({ a: { b: { c: { d: 1 } } } });
+    await obj.save();
+    obj.increment('a.b.c.d');
+    assert.strictEqual(obj.get('a').b.c.d, 2);
+
+    await obj.save();
+    assert.strictEqual(obj.get('a').b.c.d, 2);
+
+    const query = new Parse.Query(TestObject);
+    const result = await query.get(obj.id);
+    assert.strictEqual(result.get('a').b.c.d, 2);
+  });
+
   it('can increment nested field and retain full object', async () => {
     const obj = new Parse.Object('TestIncrementObject');
     obj.set('objectField', { number: 5, letter: 'a' });
diff --git a/integration/test/ParsePushTest.js b/integration/test/ParsePushTest.js
new file mode 100644
index 0000000..e34913c
--- /dev/null
+++ b/integration/test/ParsePushTest.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const Parse = require('../../node');
+
+describe('Parse Push', () => {
+  it('can get pushStatusId', async () => {
+    const payload = {
+      data: { alert: 'We return status!' },
+      where: { deviceType: { $eq: 'random' } },
+    };
+    const pushStatusId = await Parse.Push.send(payload, { useMasterKey: true });
+    const pushStatus = await Parse.Push.getPushStatus(pushStatusId, { useMasterKey: true });
+    expect(pushStatus.id).toBe(pushStatusId);
+  });
+});
diff --git a/integration/test/helper.js b/integration/test/helper.js
index 87786d8..3517b75 100644
--- a/integration/test/helper.js
+++ b/integration/test/helper.js
@@ -36,6 +36,12 @@ const defaultConfiguration = {
   },
   verbose: false,
   silent: true,
+  push: {
+    android: {
+      senderId: 'yolo',
+      apiKey: 'yolo',
+    },
+  },
   idempotencyOptions: {
     paths: ['functions/CloudFunctionIdempotency', 'jobs/CloudJob1', 'classes/IdempotentTest'],
     ttl: 120,
diff --git a/src/__tests__/Cloud-test.js b/src/__tests__/Cloud-test.js
index 370e639..b65bff2 100644
--- a/src/__tests__/Cloud-test.js
+++ b/src/__tests__/Cloud-test.js
@@ -14,9 +14,11 @@ jest.dontMock('../encode');
 jest.dontMock('../ParseError');
 jest.dontMock('../ParseObject');
 jest.dontMock('../ParseQuery');
+jest.dontMock('../Push');
 
 const Cloud = require('../Cloud');
 const CoreManager = require('../CoreManager');
+const Push = require('../Push');
 
 const defaultController = CoreManager.getCloudController();
 
@@ -304,4 +306,48 @@ describe('CloudController', () => {
     });
     expect(options.useMasterKey).toBe(true);
   });
+
+  it('can get push status', async () => {
+    const request = jest.fn();
+    request.mockReturnValue(
+      Promise.resolve({
+        results: [{ className: '_PushStatus', objectId: 'pushId1234' }],
+      })
+    );
+    CoreManager.setRESTController({ request: request, ajax: jest.fn() });
+
+    await Push.getPushStatus('pushId1234');
+    const [method, path, data, options] = request.mock.calls[0];
+    expect(method).toBe('GET');
+    expect(path).toBe('classes/_PushStatus');
+    expect(data).toEqual({
+      limit: 1,
+      where: {
+        objectId: 'pushId1234',
+      },
+    });
+    expect(options.useMasterKey).toBe(true);
+  });
+
+  it('can get push status with masterKey', async () => {
+    const request = jest.fn();
+    request.mockReturnValue(
+      Promise.resolve({
+        results: [{ className: '_PushStatus', objectId: 'pushId1234' }],
+      })
+    );
+    CoreManager.setRESTController({ request: request, ajax: jest.fn() });
+
+    await Push.getPushStatus('pushId1234', { useMasterKey: false });
+    const [method, path, data, options] = request.mock.calls[0];
+    expect(method).toBe('GET');
+    expect(path).toBe('classes/_PushStatus');
+    expect(data).toEqual({
+      limit: 1,
+      where: {
+        objectId: 'pushId1234',
+      },
+    });
+    expect(options.useMasterKey).toBe(false);
+  });
 });
diff --git a/src/__tests__/ObjectStateMutations-test.js b/src/__tests__/ObjectStateMutations-test.js
index 08c7507..82ca130 100644
--- a/src/__tests__/ObjectStateMutations-test.js
+++ b/src/__tests__/ObjectStateMutations-test.js
@@ -163,7 +163,7 @@ describe('ObjectStateMutations', () => {
   });
 
   it('can estimate attributes for nested documents', () => {
-    const serverData = { objectField: { counter: 10, letter: 'a' } };
+    let serverData = { objectField: { counter: 10, letter: 'a' } };
     let pendingOps = [{ 'objectField.counter': new ParseOps.IncrementOp(2) }];
     expect(
       ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')
@@ -182,6 +182,19 @@ describe('ObjectStateMutations', () => {
         letter: 'a',
       },
     });
+    serverData = {};
+    pendingOps = [{ 'objectField.subField.subField.counter': new ParseOps.IncrementOp(20) }];
+    expect(
+      ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')
+    ).toEqual({
+      objectField: {
+        subField: {
+          subField: {
+            counter: 20,
+          },
+        },
+      },
+    });
   });
 
   it('can commit changes from the server', () => {
diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js
index cc6ffa8..950605f 100644
--- a/src/__tests__/RESTController-test.js
+++ b/src/__tests__/RESTController-test.js
@@ -242,6 +242,29 @@ describe('RESTController', () => {
     expect(response).toBe(1234);
   });
 
+  it('handles x-parse-push-status-id header', async () => {
+    const XHR = function () {};
+    XHR.prototype = {
+      open: function () {},
+      setRequestHeader: function () {},
+      getResponseHeader: function () {
+        return 1234;
+      },
+      send: function () {
+        this.status = 200;
+        this.responseText = '{}';
+        this.readyState = 4;
+        this.onreadystatechange();
+      },
+      getAllResponseHeaders: function () {
+        return 'x-parse-push-status-id: 1234';
+      },
+    };
+    RESTController._setXHR(XHR);
+    const response = await RESTController.request('POST', 'push', {}, {});
+    expect(response).toBe(1234);
+  });
+
   it('handles invalid header', async () => {
     const XHR = function () {};
     XHR.prototype = {