Repository: parse-community/Parse-SDK-JS
Pull request #565 (2018-04-27 13:35:50)

Allow specific keys to be reverted in unsaved objects
Currently, the ```Parse.Object``` prototype method ```revert``` resets ALL unsaved changes. With this PR, specific fields can be provided to the function to allow selective reversion.

Currently:
```javascript
var person =  ParseObject.fromJSON({
      className: 'Person',
      objectId: 'xxxx',
      name: 'Omair'
 });
person.set('gender', 'male');
person.set('age', 25);
// 'age' is ignored, all fields will be reverted
person.revert('age');
// person -> { name: 'Omair' }
```

With PR (test cases in commit):
Example 1 (single field):
```javascript
var person =  ParseObject.fromJSON({
      className: 'Person',
      objectId: 'xxxx',
      name: 'Omair'
 });
person.set('gender', 'male');
person.set('age', 25);
person.revert('age');
// person -> { name: 'Omair', gender: 'male' }
```

Example 2 (multiple fields):
```javascript
var person =  ParseObject.fromJSON({
      className: 'Person',
      objectId: 'xxxx',
      name: 'Omair'
 });
person.set('gender', 'male');
person.set('age', 25);
person.set('occupation', 'Doctor');
person.revert(['gender', 'occupation']);
// person -> { name: 'Omair', age: '25' }
```

Example 3 (no fields, works as now):
```javascript
var person =  ParseObject.fromJSON({
      className: 'Person',
      objectId: 'xxxx',
      name: 'Omair'
 });
person.set('gender', 'male');
person.set('age', 25);
person.set('occupation', 'Doctor');
person.revert();
// person -> { name: 'Omair' }
```

flovilmart: Can we make this backwards compatible please? Now if no keys are passed the SDK an exception will be thrown.
omairvaiyani: @flovilmart I'm getting 584/584 passed on the branch which submitted this PR, is it failing after you attempt a merge with master?
flovilmart: LGTM’ thanks!
omairvaiyani: @flovilmart should be good to go now

Patch:
diff --git a/src/ParseObject.js b/src/ParseObject.js
index 48f4f4f..fa5faa0 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -224,10 +224,14 @@ class ParseObject {
     return stateController.getPendingOps(this._getStateIdentifier());
   }
 
-  _clearPendingOps() {
+  /**
+   * @param {Array<string>} [keysToClear] - if specified, only ops matching
+   * these fields will be cleared
+   */
+  _clearPendingOps(keysToClear?: Array<string>) {
     var pending = this._getPendingOps();
     var latest = pending[pending.length - 1];
-    var keys = Object.keys(latest);
+    var keys = keysToClear || Object.keys(latest);
     keys.forEach((key) => {
       delete latest[key];
     });
@@ -932,10 +936,22 @@ class ParseObject {
   }
 
   /**
-   * Clears any changes to this object made since the last call to save()
+   * Clears any (or specific) changes to this object made since the last call to save()
+   * @param {string} [keys] - specify which fields to revert
    */
-  revert(): void {
-    this._clearPendingOps();
+  revert(...keys: Array<string>): void {
+    let keysToRevert;
+    if(keys.length) {
+      keysToRevert = [];
+      for(const key of keys) {
+        if(typeof(key) === "string") {
+          keysToRevert.push(key);
+        } else {
+          throw new Error("Parse.Object#revert expects either no, or a list of string, arguments.");
+        }
+      }
+    }
+    this._clearPendingOps(keysToRevert);
   }
 
   /**


Test patch:
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index b96f6a6..ef0fd58 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -851,6 +851,74 @@ describe('ParseObject', () => {
     expect(o.op('count')).toBe(undefined);
   });
 
+  it('can revert a specific field in unsaved ops', () => {
+    const o = ParseObject.fromJSON({
+      className: 'Item',
+      objectId: 'canrevertspecific',
+      count: 5
+    });
+    o.set({ cool: true });
+    o.increment('count');
+    expect(o.get('cool')).toBe(true);
+    expect(o.get('count')).toBe(6);
+    o.revert('cool');
+    expect(o.get('cool')).toBe(undefined);
+    expect(o.op('cool')).toBe(undefined);
+    expect(o.get('count')).toBe(6);
+    expect(o.op('count')).not.toBe(undefined);
+  });
+
+  it('can revert multiple fields in unsaved ops', () => {
+    const o = ParseObject.fromJSON({
+      className: 'Item',
+      objectId: 'canrevertmultiple',
+      count: 5,
+      age: 18,
+      gender: 'female'
+    });
+    o.set({ cool: true, gender: 'male' });
+    o.increment('count');
+    o.increment('age');
+    expect(o.get('cool')).toBe(true);
+    expect(o.get('count')).toBe(6);
+    expect(o.get('age')).toBe(19);
+    expect(o.get('gender')).toBe('male');
+    o.revert('age', 'count', 'gender');
+    expect(o.get('cool')).toBe(true);
+    expect(o.op('cool')).not.toBe(undefined);
+    expect(o.get('count')).toBe(5);
+    expect(o.op('count')).toBe(undefined);
+    expect(o.get('age')).toBe(18);
+    expect(o.op('age')).toBe(undefined);
+    expect(o.get('gender')).toBe('female');
+    expect(o.op('gender')).toBe(undefined);
+  });
+
+  it('throws if an array is provided', () => {
+    const o = ParseObject.fromJSON({
+      className: 'Item',
+      objectId: 'throwforarray',
+      count: 5,
+      age: 18,
+      gender: 'female'
+    });
+    o.set({ cool: true, gender: 'male' });
+
+    const err = "Parse.Object#revert expects either no, or a list of string, arguments.";
+
+    expect(function() {
+      o.revert(['age'])
+    }).toThrow(err);
+
+    expect(function() {
+      o.revert([])
+    }).toThrow(err);
+
+    expect(function() {
+      o.revert('gender', ['age'])
+    }).toThrow(err);
+  });
+
   it('can fetchWithInclude', async () => {
     const objectController = CoreManager.getObjectController();
     const spy = jest.spyOn(