Skip to content

Commit 823f2d9

Browse files
committed
Merging local pending Ops when parent property also set in pending Ops
1 parent 8e189cf commit 823f2d9

File tree

3 files changed

+138
-2
lines changed

3 files changed

+138
-2
lines changed

src/ParseObject.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
AddUniqueOp,
2121
RemoveOp,
2222
RelationOp,
23+
validPendingParentOp,
24+
applyOpToParent,
2325
} from './ParseOp';
2426
import ParseRelation from './ParseRelation';
2527
import * as SingleInstanceStateController from './SingleInstanceStateController';
@@ -779,8 +781,14 @@ class ParseObject {
779781
const last = pendingOps.length - 1;
780782
const stateController = CoreManager.getObjectStateController();
781783
for (const attr in newOps) {
782-
const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]);
783-
stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp);
784+
const parentAttr = validPendingParentOp(attr, pendingOps[last]);
785+
if (parentAttr) {
786+
const parentOp = pendingOps[last][parentAttr];
787+
applyOpToParent(parentAttr, parentOp, attr, newOps[attr]);
788+
} else {
789+
const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]);
790+
stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp);
791+
}
784792
}
785793

786794
return this;

src/ParseOp.js

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

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

src/__tests__/ParseObject-test.js

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

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

0 commit comments

Comments
 (0)