Skip to content

Commit 575851c

Browse files
committed
Merging local pending Ops when parent property also set in pending Ops
1 parent 8f30edf commit 575851c

File tree

3 files changed

+138
-2
lines changed

3 files changed

+138
-2
lines changed

src/ParseObject.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
AddUniqueOp,
2525
RemoveOp,
2626
RelationOp,
27+
validPendingParentOp,
28+
applyOpToParent,
2729
} from './ParseOp';
2830
import ParseQuery from './ParseQuery';
2931
import ParseRelation from './ParseRelation';
@@ -771,8 +773,14 @@ class ParseObject {
771773
const last = pendingOps.length - 1;
772774
const stateController = CoreManager.getObjectStateController();
773775
for (const attr in newOps) {
774-
const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]);
775-
stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp);
776+
const parentAttr = validPendingParentOp(attr, pendingOps[last]);
777+
if (parentAttr) {
778+
const parentOp = pendingOps[last][parentAttr];
779+
applyOpToParent(parentAttr, parentOp, attr, newOps[attr]);
780+
} else {
781+
const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]);
782+
stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp);
783+
}
776784
}
777785

778786
return this;

src/ParseOp.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,56 @@ export function opFromJSON(json: { [key: string]: any }): ?Op {
5454
return null;
5555
}
5656

57+
export function validPendingParentOp(attr, pendingOps) {
58+
if (!pendingOps || pendingOps[attr]) {
59+
return null;
60+
}
61+
62+
const lastDot = attr.lastIndexOf('.');
63+
if (lastDot === -1) {
64+
return null;
65+
}
66+
67+
// This is an object with dot notation. So need to also match "parents"
68+
const parentString = attr.substring(0, lastDot);
69+
for (const pendingAttr in pendingOps) {
70+
if (parentString.startsWith(pendingAttr)) {
71+
return pendingAttr;
72+
}
73+
}
74+
}
75+
76+
export function applyOpToParent(parentAttr: string, parent: Op, attr: string, op: Op) {
77+
const subAttr = attr.substring(parentAttr.length + 1);
78+
79+
if (!(parent instanceof SetOp) || !subAttr) {
80+
throw new TypeError(`Trying to set sub property on a invalid property (${parentAttr} -> ${subAttr})`);
81+
}
82+
83+
let object = parent._value;
84+
const fields = subAttr.split('.');
85+
const last = fields[fields.length - 1];
86+
for (let i = 0; i < fields.length - 1; i++) {
87+
const key = fields[i];
88+
if (!(key in object)) {
89+
if (op instanceof UnsetOp) {
90+
// property already doesn't exist, we don't have to do anytihng
91+
return;
92+
}
93+
object[key] = {};
94+
}
95+
object = object[key];
96+
}
97+
98+
if (op instanceof UnsetOp) {
99+
delete object[last];
100+
} else {
101+
object[last] = op.applyTo(object[last]);
102+
}
103+
104+
return parent;
105+
}
106+
57107
export class Op {
58108
// Empty parent class
59109
applyTo(value: mixed): mixed {} /* eslint-disable-line no-unused-vars */

src/__tests__/ParseObject-test.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,84 @@ describe('ParseObject', () => {
544544
expect(o2.attributes).toEqual({});
545545
});
546546

547+
it('can set sub property of a local changed object without creating an op', () => {
548+
const o = new ParseObject('Person');
549+
o.set('data', { a: 2 });
550+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
551+
552+
o.set('datab', {v: 2});
553+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(2);
554+
555+
o.set('data.b', 3);
556+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(2);
557+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2, b: 3});
558+
559+
o.set({"data.c" : 5, "data.d.a": 4});
560+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(2);
561+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2, b: 3, c: 5, d: { a: 4 }});
562+
});
563+
564+
it('can unset sub property of a local changed object without creating an op', () => {
565+
const o = new ParseObject('Person');
566+
o.set('data', { a: 2, b: 4 });
567+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
568+
569+
o.unset('data.b');
570+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
571+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2});
572+
573+
o.unset('data.c');
574+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
575+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2});
576+
577+
o.unset('data.c.d');
578+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
579+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2});
580+
581+
o.set('data.b.c', 3);
582+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
583+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2, b: { c: 3 }});
584+
585+
o.unset('data.b.c');
586+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
587+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2, b: {}});
588+
589+
o.unset('data.b');
590+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
591+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 2});
592+
});
593+
594+
it('can increment sub property of a local changed object without creating an op', () => {
595+
const o = new ParseObject('Person');
596+
o.set('data', {a: 2, b: 4});
597+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
598+
599+
o.increment('data.a', 3);
600+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
601+
expect(o._getPendingOps()[0]['data']._value).toStrictEqual({ a: 5, b: 4});
602+
});
603+
604+
it('collapse sub-property sets with parents as well', () => {
605+
const o = new ParseObject('Person');
606+
o._finishFetch({
607+
objectId: 'o12312',
608+
data: { a: 3 }
609+
});
610+
expect(o.dirty()).toBe(false);
611+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(0);
612+
613+
o.set('data.b', { c: 1 });
614+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(1);
615+
616+
o.set('data.boo', 4);
617+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(2);
618+
expect(o._getPendingOps()[0]['data.boo']._value).toStrictEqual(4);
619+
620+
o.set('data.b.c', 2);
621+
expect(Object.keys(o._getPendingOps()[0]).length).toBe(2);
622+
expect(o._getPendingOps()[0]['data.b']._value).toStrictEqual({ c: 2 });
623+
});
624+
547625
it('can clear all fields', () => {
548626
const o = new ParseObject('Person');
549627
o._finishFetch({

0 commit comments

Comments
 (0)