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

Issue:
Calling `.add` multiple times on relations is broken
### New Issue Checklist
<!--
    Check every following box [x] before submitting your issue.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [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-SDK-JS/issues?q=is%3Aissue).
- [x] I can reproduce the issue with the latest versions of [Parse Server](https://github.com/parse-community/parse-server/releases) and the [Parse JS SDK](https://github.com/parse-community/Parse-SDK-JS/releases). <!-- We don't investigate issues for outdated releases. -->

### Issue Description
Calling `.add` multiple times on a relation adds only the object mentioned in the first call.

### Steps to reproduce
```
it('can do consecutive adds', done => {
    const ChildObject = Parse.Object.extend('ChildObject');
    const childObjects = [];
    for (let i = 0; i < 2; i++) {
      childObjects.push(new ChildObject({ x: i }));
    }
    const parent = new Parse.Object('ParentObject');
    parent.save().then(parentAgain => {
      Parse.Object.saveAll(childObjects)
        .then(() => {
          for (const child of childObjects) {
            parentAgain.relation('child').add(child);
          }
          return parentAgain.save();
        })
        .then(() => {
          return parentAgain.relation('child').query().find();
        })
        .then(results => {
          assert.equal(results.length, 2);
          const expectedIds = new Set(childObjects.map(r => r.id));
          const foundIds = new Set(results.map(r => r.id));
          assert.deepEqual(expectedIds, foundIds);
          done();
        });
    });
  });
```

### Actual Outcome
Only the first child object is added to the relation

### Expected Outcome
Both of the child objects should be added to the relation

###  Environment
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->

Server
- Parse Server version: [alpha](https://github.com/parse-community/parse-server/commit/d3087ed69f9745848729951171657b79d3192469)
- Operating system: Ubuntu
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): local

Database
- System (MongoDB or Postgres): MongoDB
- Database version: `6.0.11`
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): local

Client
- Parse JS SDK version: [alpha](https://github.com/parse-community/Parse-SDK-JS/commit/fea8edfa6c342419d06f60c686591baec2f51fc4)

### Logs
<!-- Include relevant logs here. Turn on additional logging by configuring VERBOSE=1 in your environment. -->



Discussion: 
### Thanks for opening this issue!

- 🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our [Contribution Guide](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md) for how to make a pull request, or read our [New Contributor's Guide](https://blog.parseplatform.org/learn/tutorial/community/nodejs/2021/02/14/How-to-start-contributing-to-Parse-Server.html) if this is your first time contributing.

<!-- parse-issue-bot-meta-tag-id {"suggestPr":true,"thanksFeature":false,"thanksPr":false} -->





Fix (patch):
diff --git a/src/ObjectStateMutations.js b/src/ObjectStateMutations.js
index d594d5d70..271e4f6b5 100644
--- a/src/ObjectStateMutations.js
+++ b/src/ObjectStateMutations.js
@@ -82,16 +82,15 @@ export function mergeFirstPendingState(pendingOps: Array<OpsMap>) {
 export function estimateAttribute(
   serverData: AttributeMap,
   pendingOps: Array<OpsMap>,
-  className: string,
-  id: ?string,
+  object: ParseObject,
   attr: string
 ): mixed {
   let value = serverData[attr];
   for (let i = 0; i < pendingOps.length; i++) {
     if (pendingOps[i][attr]) {
       if (pendingOps[i][attr] instanceof RelationOp) {
-        if (id) {
-          value = pendingOps[i][attr].applyTo(value, { className: className, id: id }, attr);
+        if (object.id) {
+          value = pendingOps[i][attr].applyTo(value, object, attr);
         }
       } else {
         value = pendingOps[i][attr].applyTo(value);
@@ -104,8 +103,7 @@ export function estimateAttribute(
 export function estimateAttributes(
   serverData: AttributeMap,
   pendingOps: Array<OpsMap>,
-  className: string,
-  id: ?string
+  object: ParseObject
 ): AttributeMap {
   const data = {};
   let attr;
@@ -115,12 +113,8 @@ export function estimateAttributes(
   for (let i = 0; i < pendingOps.length; i++) {
     for (attr in pendingOps[i]) {
       if (pendingOps[i][attr] instanceof RelationOp) {
-        if (id) {
-          data[attr] = pendingOps[i][attr].applyTo(
-            data[attr],
-            { className: className, id: id },
-            attr
-          );
+        if (object.id) {
+          data[attr] = pendingOps[i][attr].applyTo(data[attr], object, attr);
         }
       } else {
         if (attr.includes('.')) {
diff --git a/src/ParseOp.js b/src/ParseOp.js
index a35485c60..ed00441da 100644
--- a/src/ParseOp.js
+++ b/src/ParseOp.js
@@ -336,19 +336,13 @@ export class RelationOp extends Op {
     return obj.id;
   }
 
-  applyTo(value: mixed, object?: { className: string, id: ?string }, key?: string): ?ParseRelation {
+  applyTo(value: mixed, parent: ParseObject, key?: string): ?ParseRelation {
     if (!value) {
-      if (!object || !key) {
+      if (!parent || !key) {
         throw new Error(
           'Cannot apply a RelationOp without either a previous value, or an object and a key'
         );
       }
-      const parent = new ParseObject(object.className);
-      if (object.id && object.id.indexOf('local') === 0) {
-        parent._localId = object.id;
-      } else if (object.id) {
-        parent.id = object.id;
-      }
       const relation = new ParseRelation(parent, key);
       relation.targetClassName = this._targetClassName;
       return relation;
diff --git a/src/SingleInstanceStateController.js b/src/SingleInstanceStateController.js
index 3dd9c2cc7..6cc9d2069 100644
--- a/src/SingleInstanceStateController.js
+++ b/src/SingleInstanceStateController.js
@@ -6,6 +6,7 @@ import * as ObjectStateMutations from './ObjectStateMutations';
 
 import type { Op } from './ParseOp';
 import type { AttributeMap, ObjectCache, OpsMap, State } from './ObjectStateMutations';
+import ParseObject from './ParseObject';
 
 type ObjectIdentifier = {
   className: string,
@@ -99,22 +100,16 @@ export function getObjectCache(obj: ObjectIdentifier): ObjectCache {
   return {};
 }
 
-export function estimateAttribute(obj: ObjectIdentifier, attr: string): mixed {
+export function estimateAttribute(obj: ParseObject, attr: string): mixed {
   const serverData = getServerData(obj);
   const pendingOps = getPendingOps(obj);
-  return ObjectStateMutations.estimateAttribute(
-    serverData,
-    pendingOps,
-    obj.className,
-    obj.id,
-    attr
-  );
+  return ObjectStateMutations.estimateAttribute(serverData, pendingOps, obj, attr);
 }
 
-export function estimateAttributes(obj: ObjectIdentifier): AttributeMap {
+export function estimateAttributes(obj: ParseObject): AttributeMap {
   const serverData = getServerData(obj);
   const pendingOps = getPendingOps(obj);
-  return ObjectStateMutations.estimateAttributes(serverData, pendingOps, obj.className, obj.id);
+  return ObjectStateMutations.estimateAttributes(serverData, pendingOps, obj);
 }
 
 export function commitServerChanges(obj: ObjectIdentifier, changes: AttributeMap) {
diff --git a/src/UniqueInstanceStateController.js b/src/UniqueInstanceStateController.js
index aaf21da10..7f82777af 100644
--- a/src/UniqueInstanceStateController.js
+++ b/src/UniqueInstanceStateController.js
@@ -96,19 +96,13 @@ export function getObjectCache(obj: ParseObject): ObjectCache {
 export function estimateAttribute(obj: ParseObject, attr: string): mixed {
   const serverData = getServerData(obj);
   const pendingOps = getPendingOps(obj);
-  return ObjectStateMutations.estimateAttribute(
-    serverData,
-    pendingOps,
-    obj.className,
-    obj.id,
-    attr
-  );
+  return ObjectStateMutations.estimateAttribute(serverData, pendingOps, obj, attr);
 }
 
 export function estimateAttributes(obj: ParseObject): AttributeMap {
   const serverData = getServerData(obj);
   const pendingOps = getPendingOps(obj);
-  return ObjectStateMutations.estimateAttributes(serverData, pendingOps, obj.className, obj.id);
+  return ObjectStateMutations.estimateAttributes(serverData, pendingOps, obj);
 }
 
 export function commitServerChanges(obj: ParseObject, changes: AttributeMap) {