diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index ef3aed26a..44ae11904 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -475,6 +475,44 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').string, 'hello'); }); + it('can set and unset without save', async () => { + const obj = new TestObject({ + objectField: { + number: 5, + string: 'hello', + }, + }); + obj.unset('objectField.number'); + assert.equal(obj.get('objectField').number, undefined); + assert.equal(obj.get('objectField').string, 'hello'); + await obj.save(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').number, undefined); + assert.equal(result.get('objectField').string, 'hello'); + }); + + it('can set and set sub-property without save', async () => { + const obj = new TestObject({ + objectField: { + number: 5, + string: 'hello', + }, + }); + obj.set('objectField.numberb', 4); + assert.equal(obj.get('objectField').number, 5); + assert.equal(obj.get('objectField').numberb, 4); + assert.equal(obj.get('objectField').string, 'hello'); + await obj.save(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').number, 5); + assert.equal(result.get('objectField').numberb, 4); + assert.equal(result.get('objectField').string, 'hello'); + }); + it('can unset nested fields two levels', async () => { const obj = new TestObject({ objectField: { @@ -497,6 +535,27 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').string, 'hello'); }); + it('can unset nested fields two levels - without save between', async () => { + const obj = new TestObject({ + objectField: { + foo: { + bar: 5, + }, + string: 'hello', + }, + }); + + obj.unset('objectField.foo.bar'); + assert.equal(obj.get('objectField').foo.bar, undefined); + assert.equal(obj.get('objectField').string, 'hello'); + await obj.save(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').foo.bar, undefined); + assert.equal(result.get('objectField').string, 'hello'); + }); + it('can unset non existing fields', async () => { const obj = new TestObject(); obj.set('objectField', { number: 5 }); diff --git a/src/ParseObject.ts b/src/ParseObject.ts index 3dd26eb7f..5fc7c4766 100644 --- a/src/ParseObject.ts +++ b/src/ParseObject.ts @@ -20,6 +20,8 @@ import { AddUniqueOp, RemoveOp, RelationOp, + validPendingParentOp, + applyOpToParent, } from './ParseOp'; import ParseRelation from './ParseRelation'; import * as SingleInstanceStateController from './SingleInstanceStateController'; @@ -779,8 +781,14 @@ class ParseObject { const last = pendingOps.length - 1; const stateController = CoreManager.getObjectStateController(); for (const attr in newOps) { - const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]); - stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp); + const parentAttr = validPendingParentOp(attr, pendingOps[last]); + if (parentAttr) { + const parentOp = pendingOps[last][parentAttr]; + applyOpToParent(parentAttr, parentOp, attr, newOps[attr]); + } else { + const nextOp = newOps[attr].mergeWith(pendingOps[last][attr]); + stateController.setPendingOp(this._getStateIdentifier(), attr, nextOp); + } } return this; diff --git a/src/ParseOp.js b/src/ParseOp.js index d31a193a4..41a57bb30 100644 --- a/src/ParseOp.js +++ b/src/ParseOp.js @@ -55,6 +55,56 @@ export function opFromJSON(json: { [key: string]: any }): ?Op { return null; } +export function validPendingParentOp(attr, pendingOps) { + if (!pendingOps || pendingOps[attr]) { + return null; + } + + const lastDot = attr.lastIndexOf('.'); + if (lastDot === -1) { + return null; + } + + // This is an object with dot notation. So need to also match "parents" + const parentString = attr.substring(0, lastDot); + for (const pendingAttr in pendingOps) { + if (parentString.startsWith(pendingAttr)) { + return pendingAttr; + } + } +} + +export function applyOpToParent(parentAttr: string, parent: Op, attr: string, op: Op) { + const subAttr = attr.substring(parentAttr.length + 1); + + if (!(parent instanceof SetOp) || !subAttr) { + throw new TypeError(`Trying to set sub property on a invalid property (${parentAttr} -> ${subAttr})`); + } + + let object = parent._value; + const fields = subAttr.split('.'); + const last = fields[fields.length - 1]; + for (let i = 0; i < fields.length - 1; i++) { + const key = fields[i]; + if (!(key in object)) { + if (op instanceof UnsetOp) { + // property already doesn't exist, we don't have to do anytihng + return; + } + object[key] = {}; + } + object = object[key]; + } + + if (op instanceof UnsetOp) { + delete object[last]; + } else { + object[last] = op.applyTo(object[last]); + } + + return parent; +} + export class Op { // Empty parent class applyTo(value: any): any {} /* eslint-disable-line @typescript-eslint/no-unused-vars */ diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 0a752cee1..9585c006e 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -550,6 +550,98 @@ describe('ParseObject', () => { expect(o2.attributes).toEqual({}); }); + it('can set sub property of a local changed object without creating an op', () => { + const o = new ParseObject('Person'); + o.set('data', { a: 2 }); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + + o.set('datab', {v: 2}); + expect(Object.keys(o._getSaveJSON()).length).toBe(2); + + o.set('data.b', 3); + expect(Object.keys(o._getSaveJSON()).length).toBe(2); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2, b: 3}); + + o.set({"data.c" : 5, "data.d.a": 4}); + expect(Object.keys(o._getSaveJSON()).length).toBe(2); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2, b: 3, c: 5, d: { a: 4 }}); + }); + + it('can unset sub property of a local changed object without creating an op', () => { + const o = new ParseObject('Person'); + o.set('data', { a: 2, b: 4 }); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + + o.unset('data.b'); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2}); + + o.unset('data.c'); + expect(Object.keys(o._getPendingOps()[0]).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2}); + + o.unset('data.c.d'); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2}); + + o.set('data.b.c', 3); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2, b: { c: 3 }}); + + o.unset('data.b.c'); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2, b: {}}); + + o.unset('data.b'); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 2}); + }); + + it('can increment sub property of a local changed object without creating an op', () => { + const o = new ParseObject('Person'); + o.set('data', {a: 2, b: 4}); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + + o.increment('data.a', 3); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + expect(o._getSaveJSON()['data']).toStrictEqual({ a: 5, b: 4}); + }); + + it('collapse sub-property sets with parents as well', () => { + const o = new ParseObject('Person'); + o._finishFetch({ + objectId: 'o12312', + data: { a: 3 } + }); + expect(o.dirty()).toBe(false); + expect(Object.keys(o._getSaveJSON()).length).toBe(0); + + o.set('data.b', { c: 1 }); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + + o.set('data.boo', 4); + expect(Object.keys(o._getSaveJSON()).length).toBe(2); + expect(o._getSaveJSON()['data.boo']).toStrictEqual(4); + + o.set('data.b.c', 2); + expect(Object.keys(o._getSaveJSON()).length).toBe(2); + expect(o._getSaveJSON()['data.b']).toStrictEqual({ c: 2 }); + }); + + it('throw exception on non-sensical parent (not set)', async () => { + const o = new ParseObject('Person'); + o.increment('data', 2); + expect(Object.keys(o._getSaveJSON()).length).toBe(1); + + try { + o.set('data.a', 3); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Trying to set sub property on a invalid property (data -> a)'); + } + + }); + it('can clear all fields', () => { const o = new ParseObject('Person'); o._finishFetch({