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

Issue:
Fail to clone object with relation
How to reproduce:

```
$ npm i -g parse
/path/to/lib
└─┬ parse@1.9.2 
  └─┬ babel-runtime@6.18.0 
    ├── core-js@2.4.1 
    └── regenerator-runtime@0.9.6 

$ node
> Parse = require('parse/node')
{ ... }
> o = Parse.Object.fromJSON({objectId: '7777777777', className: "Foo", aRelation: {__type: "Relation", className: "Foo"}})
ParseObject { _objCount: 0, className: 'Foo', id: '7777777777' }
> o2 = o.clone()
ParseObject { _objCount: 1, className: 'Foo' }
> o2.relation('aRelation')
Error: Internal Error. Relation retrieved from two different Objects.
    at ParseRelation.value (/path/to/node_modules/parse/lib/node/ParseRelation.js:70:19)
    at ParseObject.value (/path/to/node_modules/parse/lib/node/ParseObject.js:630:15)
    at repl:1:4
    at REPLServer.defaultEval (repl.js:272:27)
    at bound (domain.js:280:14)
    at REPLServer.runBound [as eval] (domain.js:293:12)
    at REPLServer.<anonymous> (repl.js:441:10)
    at emitOne (events.js:101:20)
    at REPLServer.emit (events.js:188:7)
    at REPLServer.Interface._onLine (readline.js:219:10)
```

I'll send PR later.

Pull request description:
Support clone with relation (fix #381)
Fix #381 

Patch:
diff --git a/src/ParseObject.js b/src/ParseObject.js
index b2227cc..614a051 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -652,6 +652,10 @@ class ParseObject {
         !(changes[k] instanceof ParseACL)
       ) {
         newOps[k] = new SetOp(new ParseACL(changes[k]));
+      } else if (changes[k] instanceof ParseRelation) {
+        var relation = new ParseRelation(this, k);
+        relation.targetClassName = changes[k].targetClassName;
+        newOps[k] = new SetOp(relation);
       } else {
         newOps[k] = new SetOp(changes[k]);
       }
diff --git a/src/ParseOp.js b/src/ParseOp.js
index 4736897..91f7a9c 100644
--- a/src/ParseOp.js
+++ b/src/ParseOp.js
@@ -392,6 +392,8 @@ export class RelationOp extends Op {
       return this;
     } else if (previous instanceof UnsetOp) {
       throw new Error('You cannot modify a relation after deleting it.');
+    } else if (previous instanceof SetOp && previous._value instanceof ParseRelation) {
+      return this;
     } else if (previous instanceof RelationOp) {
       if (previous._targetClassName &&
         previous._targetClassName !== this._targetClassName) {


Test patch:
diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index 0941bd2..edde7e2 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -1455,4 +1455,37 @@ describe('Parse Object', () => {
       done();
     }
   });
+
+  it('can clone with relation', async (done) => {
+    const testObject = new TestObject();
+    const o = new TestObject();
+    await o.save();
+    await testObject.save();
+    let relation = o.relation('aRelation');
+    relation.add(testObject);
+    await o.save();
+
+    const o2 = o.clone();
+    assert.equal(
+      o.relation('aRelation').targetClassName,
+      o2.relation('aRelation').targetClassName
+    );
+    let relations = await o.relation('aRelation').query().find();
+    assert.equal(relations.length, 1);
+
+    relations = await o2.relation('aRelation').query().find();
+    assert.equal(relations.length, 0);
+
+    relation = o2.relation('aRelation');
+    relation.add(testObject);
+    await o2.save();
+
+    relations = await o.relation('aRelation').query().find();
+    assert.equal(relations.length, 1);
+
+    relations = await o2.relation('aRelation').query().find();
+    assert.equal(relations.length, 1);
+
+    done();
+  });
 });
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 8c11d7d..b937b55 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -22,6 +22,7 @@ jest.dontMock('../ParseFile');
 jest.dontMock('../ParseGeoPoint');
 jest.dontMock('../ParseObject');
 jest.dontMock('../ParseOp');
+jest.dontMock('../ParseRelation');
 jest.dontMock('../RESTController');
 jest.dontMock('../SingleInstanceStateController');
 jest.dontMock('../TaskQueue');
@@ -35,13 +36,48 @@ jest.dontMock('./test_helpers/mockXHR');
 jest.useFakeTimers();
 
 const mockRelation = function(parent, key) {
-  this.parentClass = parent.className;
-  this.parentId = parent.id;
+  // The parent and key fields will be populated by the parent
+  if (parent) {
+    this.parentClass = parent.className;
+    this.parentId = parent.id;
+  }
   this.key = key;
 };
 mockRelation.prototype.add = function(obj) {
   this.targetClassName = obj.className;
 };
+mockRelation.prototype.toJSON = function() {
+  return {
+    __type: 'Relation',
+    className: this.targetClassName
+  };
+};
+mockRelation.prototype._ensureParentAndKey = function(parent, key) {
+  this.key = this.key || key;
+  if (this.key !== key) {
+    throw new Error(
+      'Internal Error. Relation retrieved from two different keys.'
+    );
+  }
+  if (this.parent) {
+    if (this.parent.className !== parent.className) {
+      throw new Error(
+        'Internal Error. Relation retrieved from two different Objects.'
+      );
+    }
+    if (this.parent.id) {
+      if (this.parent.id !== parent.id) {
+        throw new Error(
+          'Internal Error. Relation retrieved from two different Objects.'
+        );
+      }
+    } else if (parent.id) {
+      this.parent = parent;
+    }
+  } else {
+    this.parent = parent;
+  }
+};
 jest.setMock('../ParseRelation', mockRelation);
 
 const mockQuery = function(className) {
@@ -514,6 +550,17 @@ describe('ParseObject', () => {
     expect(rel.targetClassName).toBe('Person');
   });
 
+  it('can be cloned with relation (#381)', () => {
+    const relationJSON = {__type: 'Relation', className: 'Bar'};
+    const o = ParseObject.fromJSON({
+      objectId: '7777777777',
+      className: 'Foo',
+      aRelation: relationJSON,
+    });
+    const o2 = o.clone();
+    expect(o2._getSaveJSON().aRelation).toEqual(relationJSON);
+  });
+
   it('can detect dirty object children', () => {
     const o = new ParseObject('Person');
     o._finishFetch({
diff --git a/src/__tests__/ParseOp-test.js b/src/__tests__/ParseOp-test.js
index be638e3..d600dfe 100644
--- a/src/__tests__/ParseOp-test.js
+++ b/src/__tests__/ParseOp-test.js
@@ -26,6 +26,13 @@ mockObject.prototype._getId = function() {
 mockObject.registerSubclass = function() {};
 jest.setMock('../ParseObject', mockObject);
 
+const mockRelation = function(parent, key) {
+  this.parent = parent;
+  this.key = key;
+}
+jest.setMock('../ParseRelation', mockRelation);
+
+const ParseRelation = require('../ParseRelation');
 const ParseObject = require('../ParseObject');
 const ParseOp = require('../ParseOp');
 const {
@@ -364,4 +371,12 @@ describe('ParseOp', () => {
       ]
     });
   });
+
+  it('can merge Relation Op with the previous Op', () => {
+    const r = new RelationOp();
+    const relation = new ParseRelation(null, null);
+    const set = new SetOp(relation);
+
+    expect(r.mergeWith(set)).toEqual(r);
+  });
 });