From 30acd9839816045464b3534103f7bb998170280e Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 6 Feb 2019 15:00:28 -0600 Subject: [PATCH 1/4] Allow dot in field names --- integration/test/ParseObjectTest.js | 15 +++++++++++++++ src/ParseObject.js | 2 +- src/__tests__/ParseObject-test.js | 4 ++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index edde7e290..380ece5f3 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -229,6 +229,21 @@ describe('Parse Object', () => { }); }); + it('can set nested fields', async () => { + const obj = new TestObject(); + obj.set('objectField', { number: 5 }); + await obj.save(); + + obj.increment('objectField.number', 15); + await obj.save(); + + assert.equal(obj.get('objectField').number, 20); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').number, 20); + }); + it('can set keys to null', (done) => { const obj = new TestObject(); obj.set('foo', null); diff --git a/src/ParseObject.js b/src/ParseObject.js index e2ce642e6..5c0de66a6 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -910,7 +910,7 @@ class ParseObject { ); } for (const key in attrs) { - if (!(/^[A-Za-z][0-9A-Za-z_]*$/).test(key)) { + if (!(/^[A-Za-z][0-9A-Za-z_.]*$/).test(key)) { return new ParseError(ParseError.INVALID_KEY_NAME); } } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 8b024017a..d2d7224e5 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -650,6 +650,10 @@ describe('ParseObject', () => { expect(o.validate({ noProblem: 'here' })).toBe(false); + + expect(o.validate({ + 'dot.field': 'here' + })).toBe(false); }); it('validates attributes on set()', () => { From c30c15c811d96aa975f28338b3a2e3d70147aa0b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 6 Feb 2019 18:14:05 -0600 Subject: [PATCH 2/4] more tests --- integration/test/ParseObjectTest.js | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 380ece5f3..c194196a4 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -229,7 +229,7 @@ describe('Parse Object', () => { }); }); - it('can set nested fields', async () => { + it('can increment nested fields', async () => { const obj = new TestObject(); obj.set('objectField', { number: 5 }); await obj.save(); @@ -244,6 +244,53 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').number, 20); }); + it('can increment nested fields without object', async () => { + const obj = new TestObject(); + obj.set('hello', 'world'); + await obj.save(); + + obj.increment('hello.dot', 15); + try { + await obj.save(); + assert.equal(false, true); + } catch(error) { + assert.equal(error.message, 'schema mismatch for TestObject.hello; expected String but got Object'); + } + }); + + it('can set nested fields', async () => { + const obj = new TestObject({ objectField: { number: 5 } }); + await obj.save(); + + assert.equal(obj.get('objectField').number, 5); + + obj.set('objectField.number', 20); + + await obj.save(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').number, 20); + }); + + it('can unset nested fields', async () => { + const obj = new TestObject({ + objectField: { + number: 5, + string: 'hello', + } + }); + await obj.save(); + + obj.unset('objectField.number'); + + 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 keys to null', (done) => { const obj = new TestObject(); obj.set('foo', null); From 5e76758a69fd87f0ba1ca42e91db4f2284f89916 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 7 Feb 2019 15:41:12 -0600 Subject: [PATCH 3/4] fix issues --- integration/test/ParseObjectTest.js | 94 +++++++++++++++++++++++++++-- src/ObjectStateMutations.js | 13 +++- src/ParseObject.js | 27 ++++++++- 3 files changed, 127 insertions(+), 7 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index c194196a4..686258cda 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -232,9 +232,11 @@ describe('Parse Object', () => { it('can increment nested fields', async () => { const obj = new TestObject(); obj.set('objectField', { number: 5 }); + assert.equal(obj.get('objectField').number, 5); await obj.save(); obj.increment('objectField.number', 15); + assert.equal(obj.get('objectField').number, 20); await obj.save(); assert.equal(obj.get('objectField').number, 20); @@ -244,6 +246,41 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').number, 20); }); + it('can increment non existing field', async () => { + const obj = new TestObject(); + obj.set('objectField', { number: 5 }); + await obj.save(); + + obj.increment('objectField.unknown', 15); + assert.deepEqual(obj.get('objectField'), { + number: 5, + unknown: 15, + }); + 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').unknown, 15); + }); + + it('can increment nested fields two levels', async () => { + const obj = new TestObject(); + obj.set('objectField', { foo: { bar: 5 } }); + assert.equal(obj.get('objectField').foo.bar, 5); + await obj.save(); + + obj.increment('objectField.foo.bar', 15); + assert.equal(obj.get('objectField').foo.bar, 20); + await obj.save(); + + assert.equal(obj.get('objectField').foo.bar, 20); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').foo.bar, 20); + }); + it('can increment nested fields without object', async () => { const obj = new TestObject(); obj.set('hello', 'world'); @@ -254,18 +291,18 @@ describe('Parse Object', () => { await obj.save(); assert.equal(false, true); } catch(error) { - assert.equal(error.message, 'schema mismatch for TestObject.hello; expected String but got Object'); + assert.equal(error.message, "Cannot create property 'dot' on string 'world'"); } }); it('can set nested fields', async () => { const obj = new TestObject({ objectField: { number: 5 } }); + assert.equal(obj.get('objectField').number, 5); await obj.save(); assert.equal(obj.get('objectField').number, 5); - obj.set('objectField.number', 20); - + assert.equal(obj.get('objectField').number, 20); await obj.save(); const query = new Parse.Query(TestObject); @@ -273,6 +310,31 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').number, 20); }); + it('ignore set nested fields on new object', async () => { + const obj = new TestObject(); + obj.set('objectField.number', 5); + assert.deepEqual(obj._getPendingOps()[0], {}); + assert.equal(obj.get('objectField'), undefined); + + await obj.save(); + assert.equal(obj.get('objectField'), undefined); + }); + + it('can set nested fields two levels', async () => { + const obj = new TestObject({ objectField: { foo: { bar: 5 } } }); + assert.equal(obj.get('objectField').foo.bar, 5); + await obj.save(); + + assert.equal(obj.get('objectField').foo.bar, 5); + obj.set('objectField.foo.bar', 20); + assert.equal(obj.get('objectField').foo.bar, 20); + await obj.save(); + + const query = new Parse.Query(TestObject); + const result = await query.get(obj.id); + assert.equal(result.get('objectField').foo.bar, 20); + }); + it('can unset nested fields', async () => { const obj = new TestObject({ objectField: { @@ -283,14 +345,38 @@ describe('Parse Object', () => { await obj.save(); 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 unset nested fields two levels', async () => { + const obj = new TestObject({ + objectField: { + foo: { + bar: 5, + }, + string: 'hello', + } + }); + await obj.save(); + + 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 set keys to null', (done) => { const obj = new TestObject(); obj.set('foo', null); diff --git a/src/ObjectStateMutations.js b/src/ObjectStateMutations.js index 4cba35663..4827449e8 100644 --- a/src/ObjectStateMutations.js +++ b/src/ObjectStateMutations.js @@ -123,7 +123,18 @@ export function estimateAttributes(serverData: AttributeMap, pendingOps: Array Date: Thu, 7 Feb 2019 17:05:50 -0600 Subject: [PATCH 4/4] improve coverage --- integration/test/ParseObjectTest.js | 27 +++++++++++++++ src/ParseObject.js | 27 +++++++++------ src/__tests__/ObjectStateMutations-test.js | 16 +++++++++ src/__tests__/ParseObject-test.js | 39 ++++++++++++++++++++++ 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 686258cda..7877a36c6 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -310,6 +310,19 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').number, 20); }); + it('can set non existing fields', async () => { + const obj = new TestObject(); + obj.set('objectField', { number: 5 }); + await obj.save(); + + obj.set('objectField.unknown', 20); + 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').unknown, 20); + }); + it('ignore set nested fields on new object', async () => { const obj = new TestObject(); obj.set('objectField.number', 5); @@ -377,6 +390,20 @@ describe('Parse Object', () => { assert.equal(result.get('objectField').string, 'hello'); }); + it('can unset non existing fields', async () => { + const obj = new TestObject(); + obj.set('objectField', { number: 5 }); + await obj.save(); + + obj.unset('objectField.unknown'); + 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').unknown, undefined); + }); + it('can set keys to null', (done) => { const obj = new TestObject(); obj.set('foo', null); diff --git a/src/ParseObject.js b/src/ParseObject.js index 587deb887..faf0b2aee 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -282,19 +282,22 @@ class ParseObject { const dirtyObjects = this._getDirtyObjectAttributes(); const json = {}; let attr; + for (attr in dirtyObjects) { - let isNested = false; - for (const field in pending[0]) { - // Nested documents aren't dirty - if (field.includes('.')) { - const fieldName = field.split('.')[0]; - if (fieldName === attr) { - isNested = true; - break; + let isDotNotation = false; + for (let i = 0; i < pending.length; i += 1) { + for (const field in pending[i]) { + // Dot notation operations are handled later + if (field.includes('.')) { + const fieldName = field.split('.')[0]; + if (fieldName === attr) { + isDotNotation = true; + break; + } } } } - if (!isNested) { + if (!isDotNotation) { json[attr] = new SetOp(dirtyObjects[attr]).toJSON(); } } @@ -595,8 +598,8 @@ class ParseObject { /** * Sets a hash of model attributes on the object. * - *

You can call it with an object containing keys and values, or with one - * key and value. For example:

+   * 

You can call it with an object containing keys and values, with one + * key and value, or dot notation. For example:

    *   gameTurn.set({
    *     player: player1,
    *     diceRoll: 2
@@ -614,6 +617,8 @@ class ParseObject {
    *
    *   game.set("finished", true);

* + * game.set("player.score", 10);

+ * * @param {String} key The key to set. * @param {} value The value to give it. * @param {Object} options A set of options for the set. diff --git a/src/__tests__/ObjectStateMutations-test.js b/src/__tests__/ObjectStateMutations-test.js index e95f181cb..9f972d22e 100644 --- a/src/__tests__/ObjectStateMutations-test.js +++ b/src/__tests__/ObjectStateMutations-test.js @@ -119,6 +119,22 @@ describe('ObjectStateMutations', () => { expect(attributes.likes.key).toBe('likes'); }); + it('can estimate attributes for nested documents', () => { + const serverData = { objectField: { counter: 10 } }; + let pendingOps = [{ 'objectField.counter': new ParseOps.IncrementOp(2) }]; + expect(ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')).toEqual({ + objectField: { + counter: 12 + }, + }); + pendingOps = [{ 'objectField.counter': new ParseOps.SetOp(20) }]; + expect(ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')).toEqual({ + objectField: { + counter: 20 + }, + }); + }); + it('can commit changes from the server', () => { const serverData = {}; const objectCache = {}; diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index d2d7224e5..6c95b170c 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -474,6 +474,45 @@ describe('ParseObject', () => { expect(o2.attributes).toEqual({ age: 41 }); }); + it('can set nested field', () => { + const o = new ParseObject('Person'); + o._finishFetch({ + objectId: 'setNested', + objectField: { + number: 5 + }, + otherField: {}, + }); + + expect(o.attributes).toEqual({ + objectField: { number: 5 }, + otherField: {}, + }); + o.set('otherField', { hello: 'world' }); + o.set('objectField.number', 20); + + expect(o.attributes).toEqual({ + objectField: { number: 20 }, + otherField: { hello: 'world' }, + }); + expect(o.op('objectField.number') instanceof SetOp).toBe(true); + expect(o.dirtyKeys()).toEqual(['otherField', 'objectField.number', 'objectField']); + expect(o._getSaveJSON()).toEqual({ + 'objectField.number': 20, + otherField: { hello: 'world' }, + }); + }); + + it('ignore set nested field on new object', () => { + const o = new ParseObject('Person'); + o.set('objectField.number', 20); + + expect(o.attributes).toEqual({}); + expect(o.op('objectField.number') instanceof SetOp).toBe(false); + expect(o.dirtyKeys()).toEqual([]); + expect(o._getSaveJSON()).toEqual({}); + }); + it('can add elements to an array field', () => { const o = new ParseObject('Schedule'); o.add('available', 'Monday');