From dbf8202c8f592f12b67206fba7e6162923deb8ba Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 15:17:29 +1000 Subject: [PATCH 01/48] Update ParseObject.js --- src/ParseObject.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 87738d56f..920c8d22b 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -162,6 +162,20 @@ class ParseObject { return Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); } + set attributes(value): AttributeMap { + const attributesNow = this.attributes; + const internal = ["createdAt", "updatedAt", "_localId", "_objCount", "sessionToken"]; + for (const key in value) { + if (internal.includes(key)) { + continue; + } + if (JSON.stringify(attributesNow[key] !== JSON.stringify(value[key]))) { + this.set(key, value[key]); + } + } + return this.attributes; + } + /** * The first time this object was saved on the server. * From f03352da035b021d188bf9c68740fe2b077d38f6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 15:37:11 +1000 Subject: [PATCH 02/48] Update ParseObject.js --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 920c8d22b..8daa32ff2 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -159,7 +159,7 @@ class ParseObject { get attributes(): AttributeMap { const stateController = CoreManager.getObjectStateController(); - return Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); + return {...stateController.estimateAttributes(this._getStateIdentifier())}; } set attributes(value): AttributeMap { From 70fcefd1caa2a53a712146f330a831bb3d37a4bc Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 20:57:21 +1000 Subject: [PATCH 03/48] feat: allow dot notation for Parse Objects with `Parse.dotNotation = true` --- integration/test/ParseObjectTest.js | 30 ++++++++++++++++ src/CoreManager.js | 1 + src/Parse.js | 11 ++++++ src/ParseObject.js | 55 ++++++++++++++++++++--------- src/__tests__/Parse-test.js | 8 +++++ src/__tests__/ParseObject-test.js | 21 +++++++++++ src/__tests__/ParseQuery-test.js | 17 +++++++++ 7 files changed, 127 insertions(+), 16 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 64e4a42e0..387f2af0c 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2089,6 +2089,36 @@ describe('Parse Object', () => { Parse.allowCustomObjectId = false; }); + fit('allow dotNotation', async () => { + Parse.dotNotation = true; + const object = new Parse.Object('TestObject'); + object.foo = 'bar'; + await object.save(); + expect(object.foo).toBe('bar'); + expect(object.get('foo')).toBe('bar'); + expect(Object.keys(object.toJSON()).sort()).toEqual([ + 'createdAt', + 'foo', + 'objectId', + 'updatedAt', + ]); + + const query = new Parse.Query('TestObject'); + const result = await query.get(object.id); + expect(result.foo).toBe('bar'); + expect(result.get('foo')).toBe('bar'); + expect(result.id).toBe(object.id); + + result.foo = 'baz'; + expect(result.get('foo')).toBe('baz'); + await result.save(); + + const afterSave = await query.get(object.id); + expect(afterSave.foo).toBe('baz'); + expect(afterSave.get('foo')).toBe('baz'); + Parse.dotNotation = false; + }); + it('allowCustomObjectId saveAll', async () => { await reconfigureServer({ allowCustomObjectId: true }); Parse.allowCustomObjectId = true; diff --git a/src/CoreManager.js b/src/CoreManager.js index 188970e5c..d103b9feb 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -201,6 +201,7 @@ const config: Config & { [key: string]: mixed } = { ENCRYPTED_USER: false, IDEMPOTENCY: false, ALLOW_CUSTOM_OBJECT_ID: false, + DOT_NOTATION: false, }; function requireMethods(name: string, methods: Array, controller: any) { diff --git a/src/Parse.js b/src/Parse.js index 7554b3c80..dd7f673f0 100644 --- a/src/Parse.js +++ b/src/Parse.js @@ -199,6 +199,17 @@ const Parse = { return CoreManager.get('IDEMPOTENCY'); }, + /** + * @member {boolean} Parse.dotNotation + * @static + */ + set dotNotation(value) { + CoreManager.set('DOT_NOTATION', value); + }, + get dotNotation() { + return CoreManager.get('DOT_NOTATION'); + }, + /** * @member {boolean} Parse.allowCustomObjectId * @static diff --git a/src/ParseObject.js b/src/ParseObject.js index 8daa32ff2..359b3e532 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -86,6 +86,40 @@ function getServerUrlPath() { const url = serverUrl.replace(/https?:\/\//, ''); return url.substr(url.indexOf('/')); } +const _internalFields = Object.freeze([ + 'objectId', + 'id', + 'className', + 'attributes', + 'createdAt', + 'updatedAt', + 'then', +]); +const proxyHandler = { + get(target, key, receiver) { + const value = target[key]; + if ( + typeof value === 'function' || + key.toString().charAt(0) === '_' || + _internalFields.includes(key.toString()) + ) { + return Reflect.get(target, key, receiver); + } + return receiver.get(key) || Reflect.get(target, key, receiver); + }, + + set(target, key, value, receiver) { + const current = target[key]; + if ( + typeof current !== 'function' && + !_internalFields.includes(key.toString()) && + key.toString().charAt(0) !== '_' + ) { + receiver.set(key, value); + } + return Reflect.set(target, key, value, receiver); + }, +}; /** * Creates a new model with defined attributes. @@ -116,6 +150,8 @@ class ParseObject { attributes?: { [attr: string]: mixed }, options?: { ignoreValidation: boolean } ) { + console.log('ISDOT=>', CoreManager.get('DOT_NOTATION')); + const proxy = CoreManager.get('DOT_NOTATION') ? new Proxy(this, proxyHandler) : this; // Enable legacy initializers if (typeof this.initialize === 'function') { this.initialize.apply(this, arguments); @@ -140,9 +176,10 @@ class ParseObject { options = attributes; } } - if (toSet && !this.set(toSet, options)) { + if (toSet && !proxy.set(toSet, options)) { throw new Error("Can't create an invalid Parse Object"); } + return proxy; } /** @@ -159,21 +196,7 @@ class ParseObject { get attributes(): AttributeMap { const stateController = CoreManager.getObjectStateController(); - return {...stateController.estimateAttributes(this._getStateIdentifier())}; - } - - set attributes(value): AttributeMap { - const attributesNow = this.attributes; - const internal = ["createdAt", "updatedAt", "_localId", "_objCount", "sessionToken"]; - for (const key in value) { - if (internal.includes(key)) { - continue; - } - if (JSON.stringify(attributesNow[key] !== JSON.stringify(value[key]))) { - this.set(key, value[key]); - } - } - return this.attributes; + return Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); } /** diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index 965668b7b..daf094a32 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -85,6 +85,14 @@ describe('Parse module', () => { expect(Parse.idempotency).toBe(false); }); + it('can set dotNotation', () => { + expect(Parse.dotNotation).toBe(false); + Parse.dotNotation = true; + expect(Parse.dotNotation).toBe(true); + Parse.dotNotation = false; + expect(Parse.dotNotation).toBe(false); + }); + it('can set LocalDatastoreController', () => { const controller = { fromPinWithName: function () {}, diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index e8294aef0..d41c9b6b4 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2568,6 +2568,27 @@ describe('ParseObject', () => { xhrs[0].onreadystatechange(); jest.runAllTicks(); }); + it('can save object with dot notation', async () => { + CoreManager.set('DOT_NOTATION', true); + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: { + objectId: 'P1', + }, + }, + ]) + ); + const obj = new ParseObject('TestObject'); + obj.name = 'Foo'; + await obj.save(); + expect(obj.name).toBe('Foo'); + expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); + expect(obj.attributes).toEqual({ name: 'Foo' }); + expect(obj.get('name')).toBe('Foo'); + CoreManager.set('DOT_NOTATION', false); + }); }); describe('ObjectController', () => { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 89150dc03..dad08436a 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -3797,4 +3797,21 @@ describe('ParseQuery LocalDatastore', () => { expect(subscription.sessionToken).toBe('r:test'); expect(subscription.query).toEqual(query); }); + + it('can query with dot notation', async () => { + CoreManager.set('DOT_NOTATION', true); + CoreManager.setQueryController({ + aggregate() {}, + find() { + return Promise.resolve({ + results: [{ objectId: 'I1', size: 'small', name: 'Product 3' }], + }); + }, + }); + const object = await new ParseQuery('Item').equalTo('size', 'small').first(); + expect(object.id).toBe('I1'); + expect(object.size).toBe('small'); + expect(object.name).toBe('Product 3'); + CoreManager.set('DOT_NOTATION', false); + }); }); From e944f91ea8bc3ece3895edc2b62e724d3a6305fa Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 20:57:49 +1000 Subject: [PATCH 04/48] Update ParseObjectTest.js --- integration/test/ParseObjectTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 387f2af0c..6bd9b2ff3 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2089,7 +2089,7 @@ describe('Parse Object', () => { Parse.allowCustomObjectId = false; }); - fit('allow dotNotation', async () => { + it('allow dotNotation', async () => { Parse.dotNotation = true; const object = new Parse.Object('TestObject'); object.foo = 'bar'; From 5044a303dd1e0647f5c301f34fddbb47c49c58bc Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 20:58:11 +1000 Subject: [PATCH 05/48] Update ParseObject.js --- src/ParseObject.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 359b3e532..04ab46d10 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -150,7 +150,6 @@ class ParseObject { attributes?: { [attr: string]: mixed }, options?: { ignoreValidation: boolean } ) { - console.log('ISDOT=>', CoreManager.get('DOT_NOTATION')); const proxy = CoreManager.get('DOT_NOTATION') ? new Proxy(this, proxyHandler) : this; // Enable legacy initializers if (typeof this.initialize === 'function') { From 75363d522c3799f75a0c768ca94968aaac9b0e98 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 21:42:38 +1000 Subject: [PATCH 06/48] Update ParseObjectTest.js --- integration/test/ParseObjectTest.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 6bd9b2ff3..9ff3bd757 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2090,8 +2090,9 @@ describe('Parse Object', () => { }); it('allow dotNotation', async () => { + await reconfigureServer({silent: false}); Parse.dotNotation = true; - const object = new Parse.Object('TestObject'); + const object = new Parse.Object('TestObject2'); object.foo = 'bar'; await object.save(); expect(object.foo).toBe('bar'); @@ -2103,7 +2104,7 @@ describe('Parse Object', () => { 'updatedAt', ]); - const query = new Parse.Query('TestObject'); + const query = new Parse.Query('TestObject2'); const result = await query.get(object.id); expect(result.foo).toBe('bar'); expect(result.get('foo')).toBe('bar'); From 302018cc3b7c381b64dec5a753c2d7a40cfe54ef Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 24 May 2022 22:41:30 +1000 Subject: [PATCH 07/48] Update ParseObject.js --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 04ab46d10..f3ce6ec4e 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -175,7 +175,7 @@ class ParseObject { options = attributes; } } - if (toSet && !proxy.set(toSet, options)) { + if (toSet && !this.set(toSet, options)) { throw new Error("Can't create an invalid Parse Object"); } return proxy; From 31fbfcce0b3fbcf1fd20add6a6efa1b185f5b763 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 25 May 2022 12:36:21 +1000 Subject: [PATCH 08/48] add user test --- integration/test/ParseObjectTest.js | 1 - integration/test/ParseUserTest.js | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 9ff3bd757..74d344445 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2090,7 +2090,6 @@ describe('Parse Object', () => { }); it('allow dotNotation', async () => { - await reconfigureServer({silent: false}); Parse.dotNotation = true; const object = new Parse.Object('TestObject2'); object.foo = 'bar'; diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index 2c405c34d..b1167cdae 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -1039,6 +1039,25 @@ describe('Parse User', () => { Parse.CoreManager.set('ENCRYPTED_KEY', null); }); + it('allow dotNotation', async () => { + Parse.dotNotation = true; + const user = new Parse.User(); + const username = uuidv4(); + user.username = username; + user.password = username; + user.foo = 'bar' + await user.signUp(); + expect(Object.keys(user.toJSON()).sort()).toEqual([ 'createdAt', 'foo', 'objectId', 'sessionToken', 'updatedAt', 'username' ]); + expect(user.username).toBe(username) + expect(user.password).toBe(username) + expect(user.foo).toBe('bar') + const userFromQuery = await new Parse.Query(Parse.User).first(); + expect(userFromQuery.username).toBe(username); + expect(userFromQuery.password).toBeUndefined(); + expect(userFromQuery.foo).toBe('bar'); + Parse.dotNotation = false; + }); + it('fix GHSA-wvh7-5p38-2qfc', async () => { Parse.User.enableUnsafeCurrentUser(); const user = new Parse.User(); From 2c5ec362196962e7f691858ec4c01b1550c816ac Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 25 May 2022 12:36:46 +1000 Subject: [PATCH 09/48] Update ParseUserTest.js --- integration/test/ParseUserTest.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index b1167cdae..5968b6f83 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -1045,12 +1045,19 @@ describe('Parse User', () => { const username = uuidv4(); user.username = username; user.password = username; - user.foo = 'bar' + user.foo = 'bar'; await user.signUp(); - expect(Object.keys(user.toJSON()).sort()).toEqual([ 'createdAt', 'foo', 'objectId', 'sessionToken', 'updatedAt', 'username' ]); - expect(user.username).toBe(username) - expect(user.password).toBe(username) - expect(user.foo).toBe('bar') + expect(Object.keys(user.toJSON()).sort()).toEqual([ + 'createdAt', + 'foo', + 'objectId', + 'sessionToken', + 'updatedAt', + 'username', + ]); + expect(user.username).toBe(username); + expect(user.password).toBe(username); + expect(user.foo).toBe('bar'); const userFromQuery = await new Parse.Query(Parse.User).first(); expect(userFromQuery.username).toBe(username); expect(userFromQuery.password).toBeUndefined(); From 953b637aa66e64a71f2eeb266b49a08405411d0a Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 26 May 2022 14:09:11 +1000 Subject: [PATCH 10/48] Update ParseObjectTest.js --- integration/test/ParseObjectTest.js | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 74d344445..84b7da6f7 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2119,6 +2119,48 @@ describe('Parse Object', () => { Parse.dotNotation = false; }); + it('allow dotNotation on pointers', async () => { + Parse.dotNotation = true; + const grandparent = new Parse.Object('DotGrandparent'); + grandparent.foo = 'bar1'; + const parent = new Parse.Object('DotParent'); + parent.foo = 'bar2'; + grandparent.parent = parent; + const child = new Parse.Object('DotChild'); + child.foo = 'bar3'; + parent.child = child; + await Parse.Object.saveAll([child, parent, grandparent]); + expect(grandparent.foo).toBe('bar1'); + expect(grandparent.parent.foo).toBe('bar2'); + expect(grandparent.parent.child.foo).toBe('bar3'); + expect(grandparent.get('foo')).toBe('bar1'); + expect(grandparent.get('parent').get('foo')).toBe('bar2'); + expect(grandparent.get('parent').get('child').get('foo')).toBe('bar3'); + expect(Object.keys(grandparent.toJSON()).sort()).toEqual([ + 'createdAt', + 'foo', + 'objectId', + 'parent', + 'updatedAt', + ]); + expect(Object.keys(grandparent.parent.toJSON()).sort()).toEqual([ + 'child', + 'createdAt', + 'foo', + 'objectId', + 'updatedAt', + ]); + expect(Object.keys(grandparent.parent.child.toJSON()).sort()).toEqual([ + 'createdAt', + 'foo', + 'objectId', + 'updatedAt', + ]); + const grandparentQuery = await new Parse.Query('DotGrandparent').include('parent', 'parent.child').first(); + expect(grandparentQuery.parent.child.foo).toEqual('bar3') + Parse.dotNotation = false; + }); + it('allowCustomObjectId saveAll', async () => { await reconfigureServer({ allowCustomObjectId: true }); Parse.allowCustomObjectId = true; From 493cd0920e05a0bff727fdd29e7a04a936394ffe Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 26 May 2022 14:45:52 +1000 Subject: [PATCH 11/48] Update ParseObject.js --- src/ParseObject.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index f3ce6ec4e..b79b5f4c4 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -1999,6 +1999,7 @@ class ParseObject { parentProto = classMap[adjustedClassName].prototype; } const ParseObjectSubclass = function (attributes, options) { + const proxy = CoreManager.get('DOT_NOTATION') ? new Proxy(this, proxyHandler) : this; this.className = adjustedClassName; this._objCount = objectCount++; // Enable legacy initializers @@ -2011,6 +2012,7 @@ class ParseObject { throw new Error("Can't create an invalid Parse Object"); } } + return proxy; }; ParseObjectSubclass.className = adjustedClassName; ParseObjectSubclass.__super__ = parentProto; From ade003a425e231e220dee3038988572bed9e0d92 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 26 May 2022 15:39:11 +1000 Subject: [PATCH 12/48] Update ParseObjectTest.js --- integration/test/ParseObjectTest.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index 84b7da6f7..4a720a32a 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2108,7 +2108,6 @@ describe('Parse Object', () => { expect(result.foo).toBe('bar'); expect(result.get('foo')).toBe('bar'); expect(result.id).toBe(object.id); - result.foo = 'baz'; expect(result.get('foo')).toBe('baz'); await result.save(); @@ -2156,8 +2155,10 @@ describe('Parse Object', () => { 'objectId', 'updatedAt', ]); - const grandparentQuery = await new Parse.Query('DotGrandparent').include('parent', 'parent.child').first(); - expect(grandparentQuery.parent.child.foo).toEqual('bar3') + const grandparentQuery = await new Parse.Query('DotGrandparent') + .include('parent', 'parent.child') + .first(); + expect(grandparentQuery.parent.child.foo).toEqual('bar3'); Parse.dotNotation = false; }); From a5751a18d53b74a2d9a91887db031992999a2b39 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 2 Jun 2022 20:54:34 +1000 Subject: [PATCH 13/48] return receiver --- src/ParseObject.js | 2 +- src/__tests__/ParseObject-test.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index b79b5f4c4..434b2da53 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -115,7 +115,7 @@ const proxyHandler = { !_internalFields.includes(key.toString()) && key.toString().charAt(0) !== '_' ) { - receiver.set(key, value); + return receiver.set(key, value); } return Reflect.set(target, key, value, receiver); }, diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index d41c9b6b4..c98716fd0 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2589,6 +2589,30 @@ describe('ParseObject', () => { expect(obj.get('name')).toBe('Foo'); CoreManager.set('DOT_NOTATION', false); }); + + it('dot notation should not assign directly', async () => { + CoreManager.set('DOT_NOTATION', true); + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: { + objectId: 'P1', + }, + }, + ]) + ); + const obj = new ParseObject('TestObject'); + obj.name = 'Foo'; + expect(Object.keys(obj)).toEqual(['_objCount', 'className', '_localId']); + await obj.save(); + expect(obj.name).toBe('Foo'); + expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); + expect(obj.attributes).toEqual({ name: 'Foo' }); + expect(obj.get('name')).toBe('Foo'); + expect(Object.keys(obj)).toEqual(['_objCount', 'className', 'id']); + CoreManager.set('DOT_NOTATION', false); + }); }); describe('ObjectController', () => { From 6aa25ec59ba8b5a332cd104dec29e385fb8714db Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Jun 2022 06:27:44 +1000 Subject: [PATCH 14/48] Update ParseUserTest.js --- integration/test/ParseUserTest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index 5968b6f83..dbbbd6ac9 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -1056,7 +1056,6 @@ describe('Parse User', () => { 'username', ]); expect(user.username).toBe(username); - expect(user.password).toBe(username); expect(user.foo).toBe('bar'); const userFromQuery = await new Parse.Query(Parse.User).first(); expect(userFromQuery.username).toBe(username); From d3857e9087d40446224c5976342cab22276e0e8a Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Jun 2022 16:02:19 +1000 Subject: [PATCH 15/48] add attribute check --- src/ParseObject.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 434b2da53..c392c73dd 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -98,14 +98,16 @@ const _internalFields = Object.freeze([ const proxyHandler = { get(target, key, receiver) { const value = target[key]; + const isAttributes = Object.keys(target.attributes).includes(key); if ( typeof value === 'function' || key.toString().charAt(0) === '_' || - _internalFields.includes(key.toString()) + _internalFields.includes(key.toString()) || + !isAttributes ) { return Reflect.get(target, key, receiver); } - return receiver.get(key) || Reflect.get(target, key, receiver); + return receiver.get(key); }, set(target, key, value, receiver) { From b73c2b893d8b4df40afd632d5c15a0fb231b0887 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Jun 2022 18:15:35 +1000 Subject: [PATCH 16/48] Update ParseObject.js --- src/ParseObject.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index c392c73dd..da9082936 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -98,16 +98,15 @@ const _internalFields = Object.freeze([ const proxyHandler = { get(target, key, receiver) { const value = target[key]; - const isAttributes = Object.keys(target.attributes).includes(key); + const reflector = Reflect.get(target, key, receiver); if ( typeof value === 'function' || key.toString().charAt(0) === '_' || - _internalFields.includes(key.toString()) || - !isAttributes + _internalFields.includes(key.toString()) ) { - return Reflect.get(target, key, receiver); + return reflector; } - return receiver.get(key); + return receiver.get(key) ?? reflector; }, set(target, key, value, receiver) { From d7bf41cc72cc02b4d1b759b80876c2b393caeae9 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 29 Aug 2022 16:40:26 +1000 Subject: [PATCH 17/48] add delete trap --- src/ParseObject.js | 52 ++++++++++++++++++------------- src/__tests__/ParseObject-test.js | 17 ++++++++-- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index da9082936..09c1a11ed 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -86,24 +86,11 @@ function getServerUrlPath() { const url = serverUrl.replace(/https?:\/\//, ''); return url.substr(url.indexOf('/')); } -const _internalFields = Object.freeze([ - 'objectId', - 'id', - 'className', - 'attributes', - 'createdAt', - 'updatedAt', - 'then', -]); const proxyHandler = { get(target, key, receiver) { const value = target[key]; const reflector = Reflect.get(target, key, receiver); - if ( - typeof value === 'function' || - key.toString().charAt(0) === '_' || - _internalFields.includes(key.toString()) - ) { + if (proxyHandler._isInternal(key, value)) { return reflector; } return receiver.get(key) ?? reflector; @@ -111,14 +98,35 @@ const proxyHandler = { set(target, key, value, receiver) { const current = target[key]; - if ( - typeof current !== 'function' && - !_internalFields.includes(key.toString()) && - key.toString().charAt(0) !== '_' - ) { - return receiver.set(key, value); - } - return Reflect.set(target, key, value, receiver); + if (proxyHandler._isInternal(key, current)) { + return Reflect.set(target, key, value, receiver); + } + return receiver.set(key, value); + }, + + deleteProperty(target, key) { + const current = target[key]; + if (proxyHandler._isInternal(key, current)) { + return delete target[key]; + } + return target.unset(key); + }, + + _isInternal(key, value) { + const internalFields = Object.freeze([ + 'objectId', + 'id', + 'className', + 'attributes', + 'createdAt', + 'updatedAt', + 'then', + ]); + return ( + typeof value === 'function' || + key.toString().charAt(0) === '_' || + internalFields.includes(key.toString()) + ); }, }; diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index c98716fd0..0eda4e9c3 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2590,6 +2590,19 @@ describe('ParseObject', () => { CoreManager.set('DOT_NOTATION', false); }); + it('can delete with dot notation', async () => { + CoreManager.set('DOT_NOTATION', true); + const obj = new ParseObject('TestObject'); + obj.name = 'Foo'; + expect(obj.attributes).toEqual({ name: 'Foo' }); + expect(obj.get('name')).toBe('Foo'); + delete obj.name; + expect(obj.op('name') instanceof ParseOp.UnsetOp).toEqual(true); + expect(obj.get('name')).toBeUndefined(); + expect(obj.attributes).toEqual({}); + CoreManager.set('DOT_NOTATION', false); + }); + it('dot notation should not assign directly', async () => { CoreManager.set('DOT_NOTATION', true); CoreManager.getRESTController()._setXHR( @@ -2597,7 +2610,7 @@ describe('ParseObject', () => { { status: 200, response: { - objectId: 'P1', + objectId: 'P2', }, }, ]) @@ -2607,7 +2620,7 @@ describe('ParseObject', () => { expect(Object.keys(obj)).toEqual(['_objCount', 'className', '_localId']); await obj.save(); expect(obj.name).toBe('Foo'); - expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); + expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P2' }); expect(obj.attributes).toEqual({ name: 'Foo' }); expect(obj.get('name')).toBe('Foo'); expect(Object.keys(obj)).toEqual(['_objCount', 'className', 'id']); From f32efeb5eb2015a2c78ba1ad6edddf6301301caa Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 9 Sep 2022 13:51:55 +1000 Subject: [PATCH 18/48] Update ParseObject.js --- src/ParseObject.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 09c1a11ed..be032efbf 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -90,16 +90,17 @@ const proxyHandler = { get(target, key, receiver) { const value = target[key]; const reflector = Reflect.get(target, key, receiver); - if (proxyHandler._isInternal(key, value)) { + if (reflector || proxyHandler._isInternal(key, value)) { return reflector; } - return receiver.get(key) ?? reflector; + return receiver.get(key); }, set(target, key, value, receiver) { const current = target[key]; - if (proxyHandler._isInternal(key, current)) { - return Reflect.set(target, key, value, receiver); + const reflector = Reflect.set(target, key, value, receiver); + if (reflector || proxyHandler._isInternal(key, current)) { + return reflector; } return receiver.set(key, value); }, From 441218c159d1b3894ccb02a8b5f35b7d8904f812 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 9 Sep 2022 13:56:44 +1000 Subject: [PATCH 19/48] Update ParseObject.js --- src/ParseObject.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index be032efbf..5b5dddc52 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -98,9 +98,8 @@ const proxyHandler = { set(target, key, value, receiver) { const current = target[key]; - const reflector = Reflect.set(target, key, value, receiver); - if (reflector || proxyHandler._isInternal(key, current)) { - return reflector; + if (proxyHandler._isInternal(key, current)) { + return Reflect.set(target, key, value, receiver); } return receiver.set(key, value); }, From 1dd49433c43a169261e2bcee823a553da1cbee77 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 6 Oct 2022 15:01:09 +1100 Subject: [PATCH 20/48] add deep --- src/ParseObject.js | 66 ++++++++++++++++++++++++++++++- src/__tests__/ParseObject-test.js | 36 +++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 5b5dddc52..7ab314b01 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -86,6 +86,50 @@ function getServerUrlPath() { const url = serverUrl.replace(/https?:\/\//, ''); return url.substr(url.indexOf('/')); } +const nestedHandler = { + updateParent(key, value) { + const levels = this._path.split('.'); + levels.push(key); + const topLevel = levels[0]; + levels.shift(); + const scope = JSON.parse(JSON.stringify(this._parent[topLevel])); + let target = scope; + const max_level = levels.length - 1; + levels.some((level, i) => { + if (typeof level === 'undefined') { + return true; + } + if (i === max_level) { + target[level] = value; + } else { + const obj = target[level] || {}; + target = obj; + } + }); + this._parent[topLevel] = scope; + }, + get(target, key, receiver) { + const reflector = Reflect.get(target, key, receiver); + const prop = target[key]; + if (Object.prototype.toString.call(prop) === '[object Object]') { + const thisHandler = { ...nestedHandler }; + thisHandler._path = `${this._path}.${key}`; + thisHandler._parent = this._parent; + return new Proxy({ ...prop }, thisHandler); + } + return reflector; + }, + set(target, key, value) { + target[key] = value; + this.updateParent(key, value); + return true; + }, + deleteProperty(target, key) { + const response = delete target[key]; + this.updateParent(key); + return response; + }, +}; const proxyHandler = { get(target, key, receiver) { const value = target[key]; @@ -93,7 +137,14 @@ const proxyHandler = { if (reflector || proxyHandler._isInternal(key, value)) { return reflector; } - return receiver.get(key); + const getValue = receiver.get(key); + if (Object.prototype.toString.call(getValue) === '[object Object]') { + const thisHandler = { ...nestedHandler }; + thisHandler._path = key; + thisHandler._parent = receiver; + return new Proxy({ ...getValue }, thisHandler); + } + return getValue ?? reflector; }, set(target, key, value, receiver) { @@ -1149,6 +1200,19 @@ class ParseObject { } } } + if (CoreManager.get('DOT_NOTATION')) { + const fields = keysToRevert && keysToRevert.length ? [...keysToRevert] : this.dirtyKeys(); + const cache = CoreManager.getObjectStateController().getObjectCache( + this._getStateIdentifier() + ); + for (const field of fields) { + try { + this[field] = encode(JSON.parse(cache[field])); + } catch (e) { + this[field] = encode(cache[field]); + } + } + } this._clearPendingOps(keysToRevert); } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 0eda4e9c3..15db6de6c 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2590,6 +2590,34 @@ describe('ParseObject', () => { CoreManager.set('DOT_NOTATION', false); }); + it('can set and revert deep with dot notation', async () => { + CoreManager.set('DOT_NOTATION', true); + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: { objectId: 'I1', nested: { foo: { a: 1 } } }, + }, + ]) + ); + const object = await new ParseObject('Test').save(); + expect(object.id).toBe('I1'); + expect(object.nested.foo).toEqual({ a: 1 }); + object.a = '123'; + object.nested.foo.a = 2; + expect(object.nested.foo).toEqual({ a: 2 }); + expect(object.dirtyKeys()).toEqual(['a', 'nested']); + object.revert('a'); + expect(object.dirtyKeys()).toEqual(['nested']); + object.revert(); + expect(object.nested.foo).toEqual({ a: 1 }); + expect(object.a).toBeUndefined(); + expect(object.dirtyKeys()).toEqual([]); + object.nested.foo.a = 2; + expect(object.nested.foo).toEqual({ a: 2 }); + CoreManager.set('DOT_NOTATION', false); + }); + it('can delete with dot notation', async () => { CoreManager.set('DOT_NOTATION', true); const obj = new ParseObject('TestObject'); @@ -2603,6 +2631,14 @@ describe('ParseObject', () => { CoreManager.set('DOT_NOTATION', false); }); + it('can delete nested keys dot notation', async () => { + CoreManager.set('DOT_NOTATION', true); + const obj = new ParseObject('TestObject', { name: { foo: { bar: 'a' } } }); + delete obj.name.foo.bar; + expect(obj.name.foo).toEqual({}); + CoreManager.set('DOT_NOTATION', false); + }); + it('dot notation should not assign directly', async () => { CoreManager.set('DOT_NOTATION', true); CoreManager.getRESTController()._setXHR( From bf5ed49399fab340c248b27ad73d9a40bfc15a9c Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 7 Oct 2022 13:07:11 +1100 Subject: [PATCH 21/48] add dirtyKeys --- src/ParseObject.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 7ab314b01..d4f2b4cc5 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -95,9 +95,10 @@ const nestedHandler = { const scope = JSON.parse(JSON.stringify(this._parent[topLevel])); let target = scope; const max_level = levels.length - 1; - levels.some((level, i) => { + for (let i = 0; i < levels.length; i++) { + const level = levels[i]; if (typeof level === 'undefined') { - return true; + break; } if (i === max_level) { target[level] = value; @@ -105,7 +106,7 @@ const nestedHandler = { const obj = target[level] || {}; target = obj; } - }); + } this._parent[topLevel] = scope; }, get(target, key, receiver) { @@ -152,7 +153,9 @@ const proxyHandler = { if (proxyHandler._isInternal(key, current)) { return Reflect.set(target, key, value, receiver); } - return receiver.set(key, value); + const returnValue = receiver.set(key, value); + receiver.dirtyKeys = receiver.dirtyKeys.bind(receiver); + return returnValue; }, deleteProperty(target, key) { @@ -1212,6 +1215,7 @@ class ParseObject { this[field] = encode(cache[field]); } } + this.dirtyKeys = this.dirtyKeys.bind(this); } this._clearPendingOps(keysToRevert); } From 505cf38f8fcae7d1b5742b97bbdd937b2f31e699 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 7 Oct 2022 17:06:55 +1100 Subject: [PATCH 22/48] Update ParseObject.js --- src/ParseObject.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index d4f2b4cc5..6b3456a19 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -1410,7 +1410,7 @@ class ParseObject { * @returns {Promise} A promise that is fulfilled when the save * completes. */ - save( + async save( arg1: ?string | { [attr: string]: mixed }, arg2: SaveOptions | mixed, arg3?: SaveOptions @@ -1452,9 +1452,10 @@ class ParseObject { } const controller = CoreManager.getObjectController(); const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; - return controller.save(unsaved, saveOptions).then(() => { - return controller.save(this, saveOptions); - }); + await controller.save(unsaved, saveOptions); + const response = await controller.save(this, saveOptions); + this.dirtyKeys = this.dirtyKeys.bind(this); + return response; } /** From 91735e84ac4a4e2f34477737145763c12fe9d142 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 10 Oct 2022 17:56:48 +1100 Subject: [PATCH 23/48] Update ParseObject.js --- src/ParseObject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 6b3456a19..3725e817c 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -112,7 +112,7 @@ const nestedHandler = { get(target, key, receiver) { const reflector = Reflect.get(target, key, receiver); const prop = target[key]; - if (Object.prototype.toString.call(prop) === '[object Object]') { + if (Object.prototype.toString.call(prop) === '[object Object]' && !prop instanceof ParseObject) { const thisHandler = { ...nestedHandler }; thisHandler._path = `${this._path}.${key}`; thisHandler._parent = this._parent; @@ -139,7 +139,7 @@ const proxyHandler = { return reflector; } const getValue = receiver.get(key); - if (Object.prototype.toString.call(getValue) === '[object Object]') { + if (Object.prototype.toString.call(getValue) === '[object Object]' && !getValue instanceof ParseObject) { const thisHandler = { ...nestedHandler }; thisHandler._path = key; thisHandler._parent = receiver; From 86cac784f2a20443cf49bbcc0638c426ea6c4d47 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 13 Oct 2022 08:29:45 +1100 Subject: [PATCH 24/48] Update ParseObject.js --- src/ParseObject.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 3725e817c..21396faff 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -101,7 +101,11 @@ const nestedHandler = { break; } if (i === max_level) { - target[level] = value; + if (value == null) { + delete target[level]; + } else { + target[level] = value; + } } else { const obj = target[level] || {}; target = obj; @@ -112,7 +116,10 @@ const nestedHandler = { get(target, key, receiver) { const reflector = Reflect.get(target, key, receiver); const prop = target[key]; - if (Object.prototype.toString.call(prop) === '[object Object]' && !prop instanceof ParseObject) { + if ( + Object.prototype.toString.call(prop) === '[object Object]' && + !(prop instanceof ParseObject) + ) { const thisHandler = { ...nestedHandler }; thisHandler._path = `${this._path}.${key}`; thisHandler._parent = this._parent; @@ -139,7 +146,10 @@ const proxyHandler = { return reflector; } const getValue = receiver.get(key); - if (Object.prototype.toString.call(getValue) === '[object Object]' && !getValue instanceof ParseObject) { + if ( + Object.prototype.toString.call(getValue) === '[object Object]' && + !(getValue instanceof ParseObject) + ) { const thisHandler = { ...nestedHandler }; thisHandler._path = key; thisHandler._parent = receiver; From 22587a0a86a8e6c8b8d38dcc369416f7f1ee769d Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 13 Oct 2022 17:45:49 +1100 Subject: [PATCH 25/48] Update ParseObject.js --- src/ParseObject.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 21396faff..98fdc9e82 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -163,9 +163,15 @@ const proxyHandler = { if (proxyHandler._isInternal(key, current)) { return Reflect.set(target, key, value, receiver); } - const returnValue = receiver.set(key, value); + if ( + Object.prototype.toString.call(value) === '[object Object]' && + value._proxy_op === 'fetch' + ) { + return true; + } + receiver.set(key, value); receiver.dirtyKeys = receiver.dirtyKeys.bind(receiver); - return returnValue; + return true; }, deleteProperty(target, key) { @@ -483,6 +489,14 @@ class ParseObject { decoded.updatedAt = decoded.createdAt; } stateController.commitServerChanges(this._getStateIdentifier(), decoded); + if (CoreManager.get('DOT_NOTATION')) { + for (const field in serverData) { + if (['objectId', 'ACL', 'createdAt', 'updatedAt'].includes(field)) { + continue; + } + this[field] = { _proxy_op: 'fetch' }; + } + } } _setExisted(existed: boolean) { @@ -1213,21 +1227,13 @@ class ParseObject { } } } + this._clearPendingOps(keysToRevert); if (CoreManager.get('DOT_NOTATION')) { - const fields = keysToRevert && keysToRevert.length ? [...keysToRevert] : this.dirtyKeys(); - const cache = CoreManager.getObjectStateController().getObjectCache( - this._getStateIdentifier() - ); - for (const field of fields) { - try { - this[field] = encode(JSON.parse(cache[field])); - } catch (e) { - this[field] = encode(cache[field]); - } + for (const field of keysToRevert) { + this[field] = { _proxy_op: 'fetch' }; } this.dirtyKeys = this.dirtyKeys.bind(this); } - this._clearPendingOps(keysToRevert); } /** From 5e6328ccaa354e1c7ccdd113eaf2dcf6730af484 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 4 Nov 2022 14:36:03 +1100 Subject: [PATCH 26/48] Update ParseObject.js --- src/ParseObject.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 98fdc9e82..0bae892e4 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -118,7 +118,7 @@ const nestedHandler = { const prop = target[key]; if ( Object.prototype.toString.call(prop) === '[object Object]' && - !(prop instanceof ParseObject) + !prop?.constructor?.name?.includes('Parse') ) { const thisHandler = { ...nestedHandler }; thisHandler._path = `${this._path}.${key}`; @@ -148,7 +148,7 @@ const proxyHandler = { const getValue = receiver.get(key); if ( Object.prototype.toString.call(getValue) === '[object Object]' && - !(getValue instanceof ParseObject) + !getValue?.constructor?.name?.includes('Parse') ) { const thisHandler = { ...nestedHandler }; thisHandler._path = key; From 878c177dd2bf35b02f25e24598491292e4fe3b8a Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 4 Nov 2022 14:56:11 +1100 Subject: [PATCH 27/48] Update ParseObject.js --- src/ParseObject.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 0bae892e4..5f0683452 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -118,6 +118,7 @@ const nestedHandler = { const prop = target[key]; if ( Object.prototype.toString.call(prop) === '[object Object]' && + !(prop instanceof ParseObject) && !prop?.constructor?.name?.includes('Parse') ) { const thisHandler = { ...nestedHandler }; @@ -148,6 +149,7 @@ const proxyHandler = { const getValue = receiver.get(key); if ( Object.prototype.toString.call(getValue) === '[object Object]' && + !(getValue instanceof ParseObject) && !getValue?.constructor?.name?.includes('Parse') ) { const thisHandler = { ...nestedHandler }; From 5197e7251eebe640273a5c4d4d99fd67547c850c Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 13 Nov 2022 17:22:41 +1100 Subject: [PATCH 28/48] Update ParseObject.js --- src/ParseObject.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 5f0683452..b5efbcbc9 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -118,8 +118,7 @@ const nestedHandler = { const prop = target[key]; if ( Object.prototype.toString.call(prop) === '[object Object]' && - !(prop instanceof ParseObject) && - !prop?.constructor?.name?.includes('Parse') + prop?.constructor?.name === 'Object' ) { const thisHandler = { ...nestedHandler }; thisHandler._path = `${this._path}.${key}`; @@ -149,8 +148,7 @@ const proxyHandler = { const getValue = receiver.get(key); if ( Object.prototype.toString.call(getValue) === '[object Object]' && - !(getValue instanceof ParseObject) && - !getValue?.constructor?.name?.includes('Parse') + getValue?.constructor?.name === 'Object' ) { const thisHandler = { ...nestedHandler }; thisHandler._path = key; From 44bcf34725b9acab2d99eb2f90f1bb84c14d3802 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Mar 2023 16:57:49 +1100 Subject: [PATCH 29/48] wip --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index cf1b47d9b..667932975 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -1222,7 +1222,7 @@ class ParseObject { } this._clearPendingOps(keysToRevert); if (CoreManager.get('DOT_NOTATION')) { - for (const field of keysToRevert) { + for (const field of keysToRevert || []) { this[field] = { _proxy_op: 'fetch' }; } this.dirtyKeys = this.dirtyKeys.bind(this); From 461f26c347e750123213a0c06ffe06c155b15f09 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Mar 2023 17:15:36 +1100 Subject: [PATCH 30/48] wip --- src/ParseObject.js | 6 +++--- src/__tests__/ParseObject-test.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 667932975..b0c634c3f 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -163,7 +163,7 @@ const proxyHandler = { return true; } receiver.set(key, value); - receiver.dirtyKeys = receiver.dirtyKeys.bind(receiver); + receiver.dirtyKeys.bind(receiver); return true; }, @@ -1225,7 +1225,7 @@ class ParseObject { for (const field of keysToRevert || []) { this[field] = { _proxy_op: 'fetch' }; } - this.dirtyKeys = this.dirtyKeys.bind(this); + this.dirtyKeys.bind(this); } } @@ -1463,7 +1463,7 @@ class ParseObject { const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; await controller.save(unsaved, saveOptions); const response = await controller.save(this, saveOptions); - this.dirtyKeys = this.dirtyKeys.bind(this); + this.dirtyKeys.bind(this); return response; } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index b37192987..213f58881 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -1861,22 +1861,22 @@ describe('ParseObject', () => { await result; }); - it('will fail for a circular dependency of non-existing objects', () => { + it('will fail for a circular dependency of non-existing objects', async () => { const parent = new ParseObject('Item'); const child = new ParseObject('Item'); parent.set('child', child); child.set('parent', parent); - expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.'); + await expect(parent.save.bind(parent)).rejects.toEqual(new Error('Cannot create a pointer to an unsaved Object.')); }); - it('will fail for deeper unsaved objects', () => { + it('will fail for deeper unsaved objects', async () => { const parent = new ParseObject('Item'); const child = new ParseObject('Item'); const grandchild = new ParseObject('Item'); parent.set('child', child); child.set('child', grandchild); - expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.'); + await expect(parent.save.bind(parent)).rejects.toEqual(new Error('Cannot create a pointer to an unsaved Object.')); }); it('does not mark shallow objects as dirty', () => { From 9b3c3c9cac167f301a53bfa89ed302af8d8198cd Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Mar 2023 18:27:42 +1100 Subject: [PATCH 31/48] bind --- src/ParseObject.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index b0c634c3f..6d84da0cd 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -163,7 +163,7 @@ const proxyHandler = { return true; } receiver.set(key, value); - receiver.dirtyKeys.bind(receiver); + receiver._bindKeys(); return true; }, @@ -500,6 +500,17 @@ class ParseObject { } } + _bindKeys() { + if (!CoreManager.get('DOT_NOTATION')) { + return; + } + const bindingKeys = ['dirtyKeys']; + for (const key of bindingKeys) { + this[key] = this[key].bind(this); + delete this[key]; + } + } + _migrateId(serverId: string) { if (this._localId && serverId) { if (singleInstance) { @@ -1225,7 +1236,7 @@ class ParseObject { for (const field of keysToRevert || []) { this[field] = { _proxy_op: 'fetch' }; } - this.dirtyKeys.bind(this); + this._bindKeys(); } } @@ -1463,7 +1474,7 @@ class ParseObject { const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; await controller.save(unsaved, saveOptions); const response = await controller.save(this, saveOptions); - this.dirtyKeys.bind(this); + this._bindKeys(); return response; } From 2be3fef751d63d003bebe72ac593bf30d562be2e Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 3 Mar 2023 18:47:33 +1100 Subject: [PATCH 32/48] tests --- src/ParseObject.js | 12 +++++++----- src/__tests__/ParseObject-test.js | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 6d84da0cd..b2bc1688d 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -1430,7 +1430,7 @@ class ParseObject { * @returns {Promise} A promise that is fulfilled when the save * completes. */ - async save( + save( arg1: ?string | { [attr: string]: mixed }, arg2: SaveOptions | mixed, arg3?: SaveOptions @@ -1472,10 +1472,12 @@ class ParseObject { } const controller = CoreManager.getObjectController(); const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; - await controller.save(unsaved, saveOptions); - const response = await controller.save(this, saveOptions); - this._bindKeys(); - return response; + return controller.save(unsaved, saveOptions).then(() => { + return controller.save(this, saveOptions); + }).then(res => { + this._bindKeys(); + return res; + }); } /** diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 213f58881..b37192987 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -1861,22 +1861,22 @@ describe('ParseObject', () => { await result; }); - it('will fail for a circular dependency of non-existing objects', async () => { + it('will fail for a circular dependency of non-existing objects', () => { const parent = new ParseObject('Item'); const child = new ParseObject('Item'); parent.set('child', child); child.set('parent', parent); - await expect(parent.save.bind(parent)).rejects.toEqual(new Error('Cannot create a pointer to an unsaved Object.')); + expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.'); }); - it('will fail for deeper unsaved objects', async () => { + it('will fail for deeper unsaved objects', () => { const parent = new ParseObject('Item'); const child = new ParseObject('Item'); const grandchild = new ParseObject('Item'); parent.set('child', child); child.set('child', grandchild); - await expect(parent.save.bind(parent)).rejects.toEqual(new Error('Cannot create a pointer to an unsaved Object.')); + expect(parent.save.bind(parent)).toThrow('Cannot create a pointer to an unsaved Object.'); }); it('does not mark shallow objects as dirty', () => { From ec61f69923990791047cdc3247fec8e8dc02842d Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 5 Mar 2023 15:56:09 +1100 Subject: [PATCH 33/48] changes --- src/ParseObject.js | 11 ++++++++++- src/__tests__/ParseObject-test.js | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index b2bc1688d..ec1401c1b 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -267,7 +267,16 @@ class ParseObject { get attributes(): AttributeMap { const stateController = CoreManager.getObjectStateController(); - return Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); + const data = Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); + return new Proxy(data, { + get: (...args) => { + return Reflect.get(...args); + }, + set: (_, prop, value) => { + this.set(prop, value); + return true; + }, + }); } /** diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index b37192987..2a619bc23 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2590,6 +2590,27 @@ describe('ParseObject', () => { CoreManager.set('DOT_NOTATION', false); }); + it('can assign attributes', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([ + { + status: 200, + response: { + objectId: 'P1', + }, + }, + ]) + ); + const obj = new ParseObject('TestObject'); + obj.attributes.name = 'Foo'; + expect(obj.attributes.name).toEqual('Foo'); + await obj.save(); + expect(obj.get('name')).toBe('Foo'); + expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); + expect(obj.attributes).toEqual({ name: 'Foo' }); + expect(obj.get('name')).toBe('Foo'); + }); + it('can set and revert deep with dot notation', async () => { CoreManager.set('DOT_NOTATION', true); CoreManager.getRESTController()._setXHR( From 2d24479d413c76d1d74a76d10ac901f6fef92846 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 5 Mar 2023 16:02:51 +1100 Subject: [PATCH 34/48] changes --- src/ParseObject.js | 114 +-------------------------------------------- src/proxy.js | 114 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 113 deletions(-) create mode 100644 src/proxy.js diff --git a/src/ParseObject.js b/src/ParseObject.js index ec1401c1b..d9803a8bb 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -14,6 +14,7 @@ import ParseError from './ParseError'; import ParseFile from './ParseFile'; import { when, continueWhile, resolvingPromise } from './promiseUtils'; import { DEFAULT_PIN, PIN_PREFIX } from './LocalDatastoreUtils'; +import proxyHandler from './proxy' import { opFromJSON, @@ -79,119 +80,6 @@ function getServerUrlPath() { const url = serverUrl.replace(/https?:\/\//, ''); return url.substr(url.indexOf('/')); } -const nestedHandler = { - updateParent(key, value) { - const levels = this._path.split('.'); - levels.push(key); - const topLevel = levels[0]; - levels.shift(); - const scope = JSON.parse(JSON.stringify(this._parent[topLevel])); - let target = scope; - const max_level = levels.length - 1; - for (let i = 0; i < levels.length; i++) { - const level = levels[i]; - if (typeof level === 'undefined') { - break; - } - if (i === max_level) { - if (value == null) { - delete target[level]; - } else { - target[level] = value; - } - } else { - const obj = target[level] || {}; - target = obj; - } - } - this._parent[topLevel] = scope; - }, - get(target, key, receiver) { - const reflector = Reflect.get(target, key, receiver); - const prop = target[key]; - if ( - Object.prototype.toString.call(prop) === '[object Object]' && - prop?.constructor?.name === 'Object' - ) { - const thisHandler = { ...nestedHandler }; - thisHandler._path = `${this._path}.${key}`; - thisHandler._parent = this._parent; - return new Proxy({ ...prop }, thisHandler); - } - return reflector; - }, - set(target, key, value) { - target[key] = value; - this.updateParent(key, value); - return true; - }, - deleteProperty(target, key) { - const response = delete target[key]; - this.updateParent(key); - return response; - }, -}; -const proxyHandler = { - get(target, key, receiver) { - const value = target[key]; - const reflector = Reflect.get(target, key, receiver); - if (reflector || proxyHandler._isInternal(key, value)) { - return reflector; - } - const getValue = receiver.get(key); - if ( - Object.prototype.toString.call(getValue) === '[object Object]' && - getValue?.constructor?.name === 'Object' - ) { - const thisHandler = { ...nestedHandler }; - thisHandler._path = key; - thisHandler._parent = receiver; - return new Proxy({ ...getValue }, thisHandler); - } - return getValue ?? reflector; - }, - - set(target, key, value, receiver) { - const current = target[key]; - if (proxyHandler._isInternal(key, current)) { - return Reflect.set(target, key, value, receiver); - } - if ( - Object.prototype.toString.call(value) === '[object Object]' && - value._proxy_op === 'fetch' - ) { - return true; - } - receiver.set(key, value); - receiver._bindKeys(); - return true; - }, - - deleteProperty(target, key) { - const current = target[key]; - if (proxyHandler._isInternal(key, current)) { - return delete target[key]; - } - return target.unset(key); - }, - - _isInternal(key, value) { - const internalFields = Object.freeze([ - 'objectId', - 'id', - 'className', - 'attributes', - 'createdAt', - 'updatedAt', - 'then', - ]); - return ( - typeof value === 'function' || - key.toString().charAt(0) === '_' || - internalFields.includes(key.toString()) - ); - }, -}; /** * Creates a new model with defined attributes. diff --git a/src/proxy.js b/src/proxy.js new file mode 100644 index 000000000..b9768b10e --- /dev/null +++ b/src/proxy.js @@ -0,0 +1,114 @@ +const nestedHandler = { + updateParent(key, value) { + const levels = this._path.split('.'); + levels.push(key); + const topLevel = levels[0]; + levels.shift(); + const scope = JSON.parse(JSON.stringify(this._parent[topLevel])); + let target = scope; + const max_level = levels.length - 1; + for (let i = 0; i < levels.length; i++) { + const level = levels[i]; + if (typeof level === 'undefined') { + break; + } + if (i === max_level) { + if (value == null) { + delete target[level]; + } else { + target[level] = value; + } + } else { + const obj = target[level] || {}; + target = obj; + } + } + this._parent[topLevel] = scope; + }, + get(target, key, receiver) { + const reflector = Reflect.get(target, key, receiver); + const prop = target[key]; + if ( + Object.prototype.toString.call(prop) === '[object Object]' && + prop?.constructor?.name === 'Object' + ) { + const thisHandler = { ...nestedHandler }; + thisHandler._path = `${this._path}.${key}`; + thisHandler._parent = this._parent; + return new Proxy({ ...prop }, thisHandler); + } + return reflector; + }, + set(target, key, value) { + target[key] = value; + this.updateParent(key, value); + return true; + }, + deleteProperty(target, key) { + const response = delete target[key]; + this.updateParent(key); + return response; + }, +}; +const proxyHandler = { + get(target, key, receiver) { + const value = target[key]; + const reflector = Reflect.get(target, key, receiver); + if (reflector || proxyHandler._isInternal(key, value)) { + return reflector; + } + const getValue = receiver.get(key); + if ( + Object.prototype.toString.call(getValue) === '[object Object]' && + getValue?.constructor?.name === 'Object' + ) { + const thisHandler = { ...nestedHandler }; + thisHandler._path = key; + thisHandler._parent = receiver; + return new Proxy({ ...getValue }, thisHandler); + } + return getValue ?? reflector; + }, + + set(target, key, value, receiver) { + const current = target[key]; + if (proxyHandler._isInternal(key, current)) { + return Reflect.set(target, key, value, receiver); + } + if ( + Object.prototype.toString.call(value) === '[object Object]' && + value._proxy_op === 'fetch' + ) { + return true; + } + receiver.set(key, value); + receiver._bindKeys(); + return true; + }, + + deleteProperty(target, key) { + const current = target[key]; + if (proxyHandler._isInternal(key, current)) { + return delete target[key]; + } + return target.unset(key); + }, + + _isInternal(key, value) { + const internalFields = Object.freeze([ + 'objectId', + 'id', + 'className', + 'attributes', + 'createdAt', + 'updatedAt', + 'then', + ]); + return ( + typeof value === 'function' || + key.toString().charAt(0) === '_' || + internalFields.includes(key.toString()) + ); + }, +}; +export default proxyHandler; From e20f550cca6257a9c546bb9947ef5a6c62e9906f Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 5 Mar 2023 16:47:09 +1100 Subject: [PATCH 35/48] change --- src/__tests__/Parse-test.js | 1 + src/__tests__/ParseObject-test.js | 1 + src/__tests__/ParseQuery-test.js | 1 + 3 files changed, 3 insertions(+) diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index 81a3f828e..e4e889103 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -6,6 +6,7 @@ jest.dontMock('../Parse'); jest.dontMock('../LocalDatastore'); jest.dontMock('crypto-js/aes'); jest.setMock('../EventuallyQueue', { poll: jest.fn() }); +jest.dontMock('../proxy'); global.indexedDB = require('./test_helpers/mockIndexedDB'); const CoreManager = require('../CoreManager'); diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 2a619bc23..7f7c68171 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -23,6 +23,7 @@ jest.dontMock('../UniqueInstanceStateController'); jest.dontMock('../unsavedChildren'); jest.dontMock('../ParseACL'); jest.dontMock('../LocalDatastore'); +jest.dontMock('../proxy'); jest.mock('../uuid', () => { let value = 0; diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 464535e28..38e804ca7 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -11,6 +11,7 @@ jest.dontMock('../ObjectStateMutations'); jest.dontMock('../LocalDatastore'); jest.dontMock('../OfflineQuery'); jest.dontMock('../LiveQuerySubscription'); +jest.dontMock('../proxy'); jest.mock('../uuid', () => { let value = 0; From 6b1716acfa2da0e2c5cb163efbcc859c4dcc0823 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 6 Mar 2023 11:08:25 +1100 Subject: [PATCH 36/48] refactor to bind --- integration/test/ParseObjectTest.js | 12 +++--- src/CoreManager.js | 1 - src/Parse.js | 11 ----- src/ParseObject.js | 63 +++++++++-------------------- src/__tests__/Parse-test.js | 8 ---- src/proxy.js | 52 ++++++------------------ 6 files changed, 36 insertions(+), 111 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index fc245eb41..ae52e7089 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2109,11 +2109,10 @@ describe('Parse Object', () => { }); it('allow dotNotation', async () => { - Parse.dotNotation = true; const object = new Parse.Object('TestObject2'); - object.foo = 'bar'; + object.bind.foo = 'bar'; await object.save(); - expect(object.foo).toBe('bar'); + expect(object.bind.foo).toBe('bar'); expect(object.get('foo')).toBe('bar'); expect(Object.keys(object.toJSON()).sort()).toEqual([ 'createdAt', @@ -2124,17 +2123,16 @@ describe('Parse Object', () => { const query = new Parse.Query('TestObject2'); const result = await query.get(object.id); - expect(result.foo).toBe('bar'); + expect(result.bind.foo).toBe('bar'); expect(result.get('foo')).toBe('bar'); expect(result.id).toBe(object.id); - result.foo = 'baz'; + result.bind.foo = 'baz'; expect(result.get('foo')).toBe('baz'); await result.save(); const afterSave = await query.get(object.id); - expect(afterSave.foo).toBe('baz'); + expect(afterSave.bind.foo).toBe('baz'); expect(afterSave.get('foo')).toBe('baz'); - Parse.dotNotation = false; }); it('allow dotNotation on pointers', async () => { diff --git a/src/CoreManager.js b/src/CoreManager.js index bce5cdb54..a751dd468 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -194,7 +194,6 @@ const config: Config & { [key: string]: mixed } = { ENCRYPTED_USER: false, IDEMPOTENCY: false, ALLOW_CUSTOM_OBJECT_ID: false, - DOT_NOTATION: false, }; function requireMethods(name: string, methods: Array, controller: any) { diff --git a/src/Parse.js b/src/Parse.js index 2a364ae6f..606ad731a 100644 --- a/src/Parse.js +++ b/src/Parse.js @@ -190,17 +190,6 @@ const Parse = { return CoreManager.get('IDEMPOTENCY'); }, - /** - * @member {boolean} Parse.dotNotation - * @static - */ - set dotNotation(value) { - CoreManager.set('DOT_NOTATION', value); - }, - get dotNotation() { - return CoreManager.get('DOT_NOTATION'); - }, - /** * @member {boolean} Parse.allowCustomObjectId * @static diff --git a/src/ParseObject.js b/src/ParseObject.js index d9803a8bb..1d39098fc 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -14,7 +14,7 @@ import ParseError from './ParseError'; import ParseFile from './ParseFile'; import { when, continueWhile, resolvingPromise } from './promiseUtils'; import { DEFAULT_PIN, PIN_PREFIX } from './LocalDatastoreUtils'; -import proxyHandler from './proxy' +import proxyHandler from './proxy'; import { opFromJSON, @@ -110,7 +110,6 @@ class ParseObject { attributes?: { [attr: string]: mixed }, options?: { ignoreValidation: boolean } ) { - const proxy = CoreManager.get('DOT_NOTATION') ? new Proxy(this, proxyHandler) : this; // Enable legacy initializers if (typeof this.initialize === 'function') { this.initialize.apply(this, arguments); @@ -138,7 +137,7 @@ class ParseObject { if (toSet && !this.set(toSet, options)) { throw new Error("Can't create an invalid Parse Object"); } - return proxy; + this._createProxy(); } /** @@ -155,16 +154,7 @@ class ParseObject { get attributes(): AttributeMap { const stateController = CoreManager.getObjectStateController(); - const data = Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); - return new Proxy(data, { - get: (...args) => { - return Reflect.get(...args); - }, - set: (_, prop, value) => { - this.set(prop, value); - return true; - }, - }); + return Object.freeze(stateController.estimateAttributes(this._getStateIdentifier())); } /** @@ -379,14 +369,7 @@ class ParseObject { decoded.updatedAt = decoded.createdAt; } stateController.commitServerChanges(this._getStateIdentifier(), decoded); - if (CoreManager.get('DOT_NOTATION')) { - for (const field in serverData) { - if (['objectId', 'ACL', 'createdAt', 'updatedAt'].includes(field)) { - continue; - } - this[field] = { _proxy_op: 'fetch' }; - } - } + this._createProxy(); } _setExisted(existed: boolean) { @@ -397,15 +380,8 @@ class ParseObject { } } - _bindKeys() { - if (!CoreManager.get('DOT_NOTATION')) { - return; - } - const bindingKeys = ['dirtyKeys']; - for (const key of bindingKeys) { - this[key] = this[key].bind(this); - delete this[key]; - } + _createProxy() { + this.bind = new Proxy(this, proxyHandler); } _migrateId(serverId: string) { @@ -945,7 +921,7 @@ class ParseObject { */ op(attr: string): ?Op { const pending = this._getPendingOps(); - for (let i = pending.length; i--;) { + for (let i = pending.length; i--; ) { if (pending[i][attr]) { return pending[i][attr]; } @@ -1129,12 +1105,7 @@ class ParseObject { } } this._clearPendingOps(keysToRevert); - if (CoreManager.get('DOT_NOTATION')) { - for (const field of keysToRevert || []) { - this[field] = { _proxy_op: 'fetch' }; - } - this._bindKeys(); - } + this._createProxy(); } /** @@ -1369,12 +1340,15 @@ class ParseObject { } const controller = CoreManager.getObjectController(); const unsaved = options.cascadeSave !== false ? unsavedChildren(this) : null; - return controller.save(unsaved, saveOptions).then(() => { - return controller.save(this, saveOptions); - }).then(res => { - this._bindKeys(); - return res; - }); + return controller + .save(unsaved, saveOptions) + .then(() => { + return controller.save(this, saveOptions); + }) + .then(res => { + this._createProxy(); + return res; + }); } /** @@ -1994,7 +1968,6 @@ class ParseObject { parentProto = this.prototype; } let ParseObjectSubclass = function (attributes, options) { - const proxy = CoreManager.get('DOT_NOTATION') ? new Proxy(this, proxyHandler) : this; this.className = adjustedClassName; this._objCount = objectCount++; // Enable legacy initializers @@ -2013,7 +1986,7 @@ class ParseObject { throw new Error("Can't create an invalid Parse Object"); } } - return proxy; + this._createProxy(); }; if (classMap[adjustedClassName]) { ParseObjectSubclass = classMap[adjustedClassName]; diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index e4e889103..2133ac930 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -78,14 +78,6 @@ describe('Parse module', () => { expect(Parse.idempotency).toBe(false); }); - it('can set dotNotation', () => { - expect(Parse.dotNotation).toBe(false); - Parse.dotNotation = true; - expect(Parse.dotNotation).toBe(true); - Parse.dotNotation = false; - expect(Parse.dotNotation).toBe(false); - }); - it('can set LocalDatastoreController', () => { const controller = { fromPinWithName: function () {}, diff --git a/src/proxy.js b/src/proxy.js index b9768b10e..8ac15a876 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -52,12 +52,7 @@ const nestedHandler = { }; const proxyHandler = { get(target, key, receiver) { - const value = target[key]; - const reflector = Reflect.get(target, key, receiver); - if (reflector || proxyHandler._isInternal(key, value)) { - return reflector; - } - const getValue = receiver.get(key); + const getValue = target.get(key); if ( Object.prototype.toString.call(getValue) === '[object Object]' && getValue?.constructor?.name === 'Object' @@ -67,48 +62,27 @@ const proxyHandler = { thisHandler._parent = receiver; return new Proxy({ ...getValue }, thisHandler); } - return getValue ?? reflector; + return getValue; }, - set(target, key, value, receiver) { - const current = target[key]; - if (proxyHandler._isInternal(key, current)) { - return Reflect.set(target, key, value, receiver); - } - if ( - Object.prototype.toString.call(value) === '[object Object]' && - value._proxy_op === 'fetch' - ) { - return true; - } - receiver.set(key, value); - receiver._bindKeys(); + set(target, key, value) { + target.set(key, value); return true; }, deleteProperty(target, key) { - const current = target[key]; - if (proxyHandler._isInternal(key, current)) { - return delete target[key]; - } return target.unset(key); }, + ownKeys(target) { + // called once to get a list of properties + return Object.keys(target.attributes); + }, - _isInternal(key, value) { - const internalFields = Object.freeze([ - 'objectId', - 'id', - 'className', - 'attributes', - 'createdAt', - 'updatedAt', - 'then', - ]); - return ( - typeof value === 'function' || - key.toString().charAt(0) === '_' || - internalFields.includes(key.toString()) - ); + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + }; }, }; export default proxyHandler; From d8da6e11a7963e17851754ca09590318cb3b337c Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 6 Mar 2023 12:00:49 +1100 Subject: [PATCH 37/48] refactor to `bind` --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 1d39098fc..0ee691bf2 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -921,7 +921,7 @@ class ParseObject { */ op(attr: string): ?Op { const pending = this._getPendingOps(); - for (let i = pending.length; i--; ) { + for (let i = pending.length; i--;) { if (pending[i][attr]) { return pending[i][attr]; } From dfc7b6fa8f22a164d06c39bfabb38a868d279c38 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 6 Mar 2023 12:05:51 +1100 Subject: [PATCH 38/48] refactor tests --- src/__tests__/ParseObject-test.js | 81 ++++++------------------------- src/__tests__/ParseQuery-test.js | 6 +-- 2 files changed, 17 insertions(+), 70 deletions(-) diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 7f7c68171..eed8909c1 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -2570,7 +2570,6 @@ describe('ParseObject', () => { }); }); it('can save object with dot notation', async () => { - CoreManager.set('DOT_NOTATION', true); CoreManager.getRESTController()._setXHR( mockXHR([ { @@ -2582,38 +2581,16 @@ describe('ParseObject', () => { ]) ); const obj = new ParseObject('TestObject'); - obj.name = 'Foo'; + obj.bind.name = 'Foo'; + expect(Object.keys(obj.bind)).toEqual(['name']) await obj.save(); - expect(obj.name).toBe('Foo'); - expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); - expect(obj.attributes).toEqual({ name: 'Foo' }); - expect(obj.get('name')).toBe('Foo'); - CoreManager.set('DOT_NOTATION', false); - }); - - it('can assign attributes', async () => { - CoreManager.getRESTController()._setXHR( - mockXHR([ - { - status: 200, - response: { - objectId: 'P1', - }, - }, - ]) - ); - const obj = new ParseObject('TestObject'); - obj.attributes.name = 'Foo'; - expect(obj.attributes.name).toEqual('Foo'); - await obj.save(); - expect(obj.get('name')).toBe('Foo'); + expect(obj.bind.name).toBe('Foo'); expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P1' }); expect(obj.attributes).toEqual({ name: 'Foo' }); expect(obj.get('name')).toBe('Foo'); }); it('can set and revert deep with dot notation', async () => { - CoreManager.set('DOT_NOTATION', true); CoreManager.getRESTController()._setXHR( mockXHR([ { @@ -2624,66 +2601,38 @@ describe('ParseObject', () => { ); const object = await new ParseObject('Test').save(); expect(object.id).toBe('I1'); - expect(object.nested.foo).toEqual({ a: 1 }); - object.a = '123'; - object.nested.foo.a = 2; - expect(object.nested.foo).toEqual({ a: 2 }); + expect(object.bind.nested.foo).toEqual({ a: 1 }); + object.bind.a = '123'; + object.bind.nested.foo.a = 2; + expect(object.bind.nested.foo).toEqual({ a: 2 }); expect(object.dirtyKeys()).toEqual(['a', 'nested']); object.revert('a'); expect(object.dirtyKeys()).toEqual(['nested']); object.revert(); - expect(object.nested.foo).toEqual({ a: 1 }); - expect(object.a).toBeUndefined(); + expect(object.bind.nested.foo).toEqual({ a: 1 }); + expect(object.bind.a).toBeUndefined(); expect(object.dirtyKeys()).toEqual([]); - object.nested.foo.a = 2; - expect(object.nested.foo).toEqual({ a: 2 }); - CoreManager.set('DOT_NOTATION', false); + object.bind.nested.foo.a = 2; + expect(object.bind.nested.foo).toEqual({ a: 2 }); }); it('can delete with dot notation', async () => { - CoreManager.set('DOT_NOTATION', true); const obj = new ParseObject('TestObject'); - obj.name = 'Foo'; + obj.bind.name = 'Foo'; expect(obj.attributes).toEqual({ name: 'Foo' }); expect(obj.get('name')).toBe('Foo'); - delete obj.name; + delete obj.bind.name; expect(obj.op('name') instanceof ParseOp.UnsetOp).toEqual(true); expect(obj.get('name')).toBeUndefined(); expect(obj.attributes).toEqual({}); - CoreManager.set('DOT_NOTATION', false); }); it('can delete nested keys dot notation', async () => { - CoreManager.set('DOT_NOTATION', true); const obj = new ParseObject('TestObject', { name: { foo: { bar: 'a' } } }); - delete obj.name.foo.bar; - expect(obj.name.foo).toEqual({}); - CoreManager.set('DOT_NOTATION', false); + delete obj.bind.name.foo.bar; + expect(obj.bind.name.foo).toEqual({}); }); - it('dot notation should not assign directly', async () => { - CoreManager.set('DOT_NOTATION', true); - CoreManager.getRESTController()._setXHR( - mockXHR([ - { - status: 200, - response: { - objectId: 'P2', - }, - }, - ]) - ); - const obj = new ParseObject('TestObject'); - obj.name = 'Foo'; - expect(Object.keys(obj)).toEqual(['_objCount', 'className', '_localId']); - await obj.save(); - expect(obj.name).toBe('Foo'); - expect(obj.toJSON()).toEqual({ name: 'Foo', objectId: 'P2' }); - expect(obj.attributes).toEqual({ name: 'Foo' }); - expect(obj.get('name')).toBe('Foo'); - expect(Object.keys(obj)).toEqual(['_objCount', 'className', 'id']); - CoreManager.set('DOT_NOTATION', false); - }); }); describe('ObjectController', () => { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 38e804ca7..980462b33 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -3800,7 +3800,6 @@ describe('ParseQuery LocalDatastore', () => { }); it('can query with dot notation', async () => { - CoreManager.set('DOT_NOTATION', true); CoreManager.setQueryController({ aggregate() {}, find() { @@ -3811,8 +3810,7 @@ describe('ParseQuery LocalDatastore', () => { }); const object = await new ParseQuery('Item').equalTo('size', 'small').first(); expect(object.id).toBe('I1'); - expect(object.size).toBe('small'); - expect(object.name).toBe('Product 3'); - CoreManager.set('DOT_NOTATION', false); + expect(object.bind.size).toBe('small'); + expect(object.bind.name).toBe('Product 3'); }); }); From 65e2b0ded40637a366f4dc53ca11d42016d812b8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 6 Mar 2023 12:24:39 +1100 Subject: [PATCH 39/48] integration tests --- integration/test/ParseObjectTest.js | 26 ++++++++++++-------------- integration/test/ParseUserTest.js | 20 +++++++++----------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index ae52e7089..a2721f8d2 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2108,7 +2108,7 @@ describe('Parse Object', () => { }); }); - it('allow dotNotation', async () => { + it('allow binding', async () => { const object = new Parse.Object('TestObject2'); object.bind.foo = 'bar'; await object.save(); @@ -2135,20 +2135,19 @@ describe('Parse Object', () => { expect(afterSave.get('foo')).toBe('baz'); }); - it('allow dotNotation on pointers', async () => { - Parse.dotNotation = true; + it('allow binding on pointers', async () => { const grandparent = new Parse.Object('DotGrandparent'); - grandparent.foo = 'bar1'; + grandparent.bind.foo = 'bar1'; const parent = new Parse.Object('DotParent'); - parent.foo = 'bar2'; - grandparent.parent = parent; + parent.bind.foo = 'bar2'; + grandparent.bind.parent = parent; const child = new Parse.Object('DotChild'); child.foo = 'bar3'; - parent.child = child; + parent.bind.child = child; await Parse.Object.saveAll([child, parent, grandparent]); - expect(grandparent.foo).toBe('bar1'); - expect(grandparent.parent.foo).toBe('bar2'); - expect(grandparent.parent.child.foo).toBe('bar3'); + expect(grandparent.bind.foo).toBe('bar1'); + expect(grandparent.bind.parent.bind.foo).toBe('bar2'); + expect(grandparent.bind.parent.bind.child.bind.foo).toBe('bar3'); expect(grandparent.get('foo')).toBe('bar1'); expect(grandparent.get('parent').get('foo')).toBe('bar2'); expect(grandparent.get('parent').get('child').get('foo')).toBe('bar3'); @@ -2159,14 +2158,14 @@ describe('Parse Object', () => { 'parent', 'updatedAt', ]); - expect(Object.keys(grandparent.parent.toJSON()).sort()).toEqual([ + expect(Object.keys(grandparent.bind.parent.toJSON()).sort()).toEqual([ 'child', 'createdAt', 'foo', 'objectId', 'updatedAt', ]); - expect(Object.keys(grandparent.parent.child.toJSON()).sort()).toEqual([ + expect(Object.keys(grandparent.bind.parent.bind.child.toJSON()).sort()).toEqual([ 'createdAt', 'foo', 'objectId', @@ -2175,8 +2174,7 @@ describe('Parse Object', () => { const grandparentQuery = await new Parse.Query('DotGrandparent') .include('parent', 'parent.child') .first(); - expect(grandparentQuery.parent.child.foo).toEqual('bar3'); - Parse.dotNotation = false; + expect(grandparentQuery.bind.parent.bind.child.bind.foo).toEqual('bar3'); }); describe('allowCustomObjectId saveAll', () => { diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index 527e9c03f..6a041a3a5 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -1042,13 +1042,12 @@ describe('Parse User', () => { Parse.CoreManager.set('ENCRYPTED_KEY', null); }); - it('allow dotNotation', async () => { - Parse.dotNotation = true; + it('allow binding', async () => { const user = new Parse.User(); const username = uuidv4(); - user.username = username; - user.password = username; - user.foo = 'bar'; + user.bind.username = username; + user.bind.password = username; + user.bind.foo = 'bar'; await user.signUp(); expect(Object.keys(user.toJSON()).sort()).toEqual([ 'createdAt', @@ -1058,13 +1057,12 @@ describe('Parse User', () => { 'updatedAt', 'username', ]); - expect(user.username).toBe(username); - expect(user.foo).toBe('bar'); + expect(user.bind.username).toBe(username); + expect(user.bind.foo).toBe('bar'); const userFromQuery = await new Parse.Query(Parse.User).first(); - expect(userFromQuery.username).toBe(username); - expect(userFromQuery.password).toBeUndefined(); - expect(userFromQuery.foo).toBe('bar'); - Parse.dotNotation = false; + expect(userFromQuery.bind.username).toBe(username); + expect(userFromQuery.bind.password).toBeUndefined(); + expect(userFromQuery.bind.foo).toBe('bar'); }); it('fix GHSA-wvh7-5p38-2qfc', async () => { From ab99429b00bb8047b2bc99f406b446368f399bb5 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 6 Mar 2023 12:31:31 +1100 Subject: [PATCH 40/48] Update ParseObjectTest.js --- integration/test/ParseObjectTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js index a2721f8d2..2fe64128b 100644 --- a/integration/test/ParseObjectTest.js +++ b/integration/test/ParseObjectTest.js @@ -2142,7 +2142,7 @@ describe('Parse Object', () => { parent.bind.foo = 'bar2'; grandparent.bind.parent = parent; const child = new Parse.Object('DotChild'); - child.foo = 'bar3'; + child.bind.foo = 'bar3'; parent.bind.child = child; await Parse.Object.saveAll([child, parent, grandparent]); expect(grandparent.bind.foo).toBe('bar1'); From db4f0c77a921d2005fbaff8aa377bd59dd84369c Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 7 Mar 2023 12:33:06 +1100 Subject: [PATCH 41/48] Update ParseObject.js --- src/ParseObject.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 0ee691bf2..5577ce708 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -381,6 +381,7 @@ class ParseObject { } _createProxy() { + this.bind = Object.assign({}, this.attributes); this.bind = new Proxy(this, proxyHandler); } From e5e08880d3f5661325c3370843724e6f56d0f5e8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 7 Mar 2023 12:45:25 +1100 Subject: [PATCH 42/48] svelte --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 5577ce708..620b52f8e 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -381,7 +381,6 @@ class ParseObject { } _createProxy() { - this.bind = Object.assign({}, this.attributes); this.bind = new Proxy(this, proxyHandler); } @@ -1107,6 +1106,7 @@ class ParseObject { } this._clearPendingOps(keysToRevert); this._createProxy(); + return this; } /** From a744bf47d69c76f0e0cfb092474bd3f959c88d05 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Mar 2023 11:10:25 +1100 Subject: [PATCH 43/48] Update ParseObject.js --- src/ParseObject.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ParseObject.js b/src/ParseObject.js index 620b52f8e..28f4542f7 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -150,6 +150,17 @@ class ParseObject { _objCount: number; className: string; + /** + * Bind, used for two way directonal binding using + * + * When using a responsive framework that supports binding to an object's keys, use `object.bind.key` for dynamic updating of a Parse.Object + * + * `object.get("key")` and `object.set("set")` is preffered for one way binding. + * + * @property {object} id + */ + bind: AttributeMap; + /* Prototype getters / setters */ get attributes(): AttributeMap { From d5cfb9700503e77a288147367769371df15efbb4 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Mar 2023 11:10:34 +1100 Subject: [PATCH 44/48] Update ParseObject.js --- src/ParseObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParseObject.js b/src/ParseObject.js index 28f4542f7..1abe86577 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -157,7 +157,7 @@ class ParseObject { * * `object.get("key")` and `object.set("set")` is preffered for one way binding. * - * @property {object} id + * @property {object} bind */ bind: AttributeMap; From 693a4834cc77be5168f8b126931e7a5c557455f1 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 14 Mar 2023 15:21:38 +1100 Subject: [PATCH 45/48] Update proxy.js --- src/proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy.js b/src/proxy.js index 8ac15a876..f36e730c0 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -4,7 +4,7 @@ const nestedHandler = { levels.push(key); const topLevel = levels[0]; levels.shift(); - const scope = JSON.parse(JSON.stringify(this._parent[topLevel])); + const scope = structuredClone(this._parent[topLevel]); let target = scope; const max_level = levels.length - 1; for (let i = 0; i < levels.length; i++) { From c1904fce02b3a808dcb9f91ec1778407b1a40932 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 14 Mar 2023 15:35:29 +1100 Subject: [PATCH 46/48] add copy --- package-lock.json | 9 +++------ package.json | 1 + src/proxy.js | 4 +++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d1026a6b..70fe3c9e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "7.21.0", + "deepcopy": "2.1.0", "idb-keyval": "6.2.0", "react-native-crypto-js": "1.0.0", "uuid": "9.0.0", @@ -8696,7 +8697,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.1.0.tgz", "integrity": "sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==", - "dev": true, "dependencies": { "type-detect": "^4.0.8" } @@ -26343,7 +26343,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } @@ -34223,7 +34222,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/deepcopy/-/deepcopy-2.1.0.tgz", "integrity": "sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==", - "dev": true, "requires": { "type-detect": "^4.0.8" } @@ -44866,7 +44864,7 @@ "version": "git+ssh://git@github.com/parse-community/parse-server.git#0f1979f814f69b8994cbf84949f6dcf659053d26", "integrity": "sha512-YOBKTFBp1nPZUXY71TkC/V1NWDGHQy+NSJY76+DNoe7liY23p/qHaIaqcRkhWPzxmhWaJWNMk8fE2rD6JzQdeA==", "dev": true, - "from": "parse-server@git+https://github.com/parse-community/parse-server#0f1979f814f69b8994cbf84949f6dcf659053d26", + "from": "parse-server@git+https://github.com/parse-community/parse-server#alpha", "requires": { "@babel/eslint-parser": "7.19.1", "@graphql-tools/merge": "8.3.6", @@ -48006,8 +48004,7 @@ "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" }, "type-fest": { "version": "0.18.1", diff --git a/package.json b/package.json index c0d27e376..c10b5a5fc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@babel/runtime-corejs3": "7.21.0", + "deepcopy": "2.1.0", "idb-keyval": "6.2.0", "react-native-crypto-js": "1.0.0", "uuid": "9.0.0", diff --git a/src/proxy.js b/src/proxy.js index f36e730c0..3d5482daf 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -1,10 +1,12 @@ +import * as deepcopy from 'deepcopy'; const nestedHandler = { updateParent(key, value) { const levels = this._path.split('.'); levels.push(key); const topLevel = levels[0]; levels.shift(); - const scope = structuredClone(this._parent[topLevel]); + const copiedParent = Array.isArray(this._parent[topLevel]) ? [...this._parent[topLevel]] : {...this._parent[topLevel]}; + const scope = deepcopy(copiedParent); let target = scope; const max_level = levels.length - 1; for (let i = 0; i < levels.length; i++) { From 4b95df3b11ca2cd7deb3011a679588198edc9cd8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 14 Mar 2023 16:00:21 +1100 Subject: [PATCH 47/48] arrays --- src/__tests__/Parse-test.js | 1 + src/__tests__/ParseObject-test.js | 7 +++++++ src/__tests__/ParseQuery-test.js | 1 + src/proxy.js | 25 +++++++++++++++---------- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index 2133ac930..256f78eab 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -7,6 +7,7 @@ jest.dontMock('../LocalDatastore'); jest.dontMock('crypto-js/aes'); jest.setMock('../EventuallyQueue', { poll: jest.fn() }); jest.dontMock('../proxy'); +jest.dontMock('deepcopy'); global.indexedDB = require('./test_helpers/mockIndexedDB'); const CoreManager = require('../CoreManager'); diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index eed8909c1..b17212b5e 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -24,6 +24,7 @@ jest.dontMock('../unsavedChildren'); jest.dontMock('../ParseACL'); jest.dontMock('../LocalDatastore'); jest.dontMock('../proxy'); +jest.dontMock('deepcopy'); jest.mock('../uuid', () => { let value = 0; @@ -2633,6 +2634,12 @@ describe('ParseObject', () => { expect(obj.bind.name.foo).toEqual({}); }); + it('can update nested array with dot notation', async () => { + const obj = new ParseObject('TestObject', { name: [{foo: { bar: 'a' } }] }); + obj.bind.name[0].foo.bar = 'b'; + expect(obj.get('name')).toEqual([{foo: { bar: 'b' } }]); + }); + }); describe('ObjectController', () => { diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 980462b33..05a277acc 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -12,6 +12,7 @@ jest.dontMock('../LocalDatastore'); jest.dontMock('../OfflineQuery'); jest.dontMock('../LiveQuerySubscription'); jest.dontMock('../proxy'); +jest.dontMock('deepcopy'); jest.mock('../uuid', () => { let value = 0; diff --git a/src/proxy.js b/src/proxy.js index 3d5482daf..aa1932645 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -1,18 +1,19 @@ -import * as deepcopy from 'deepcopy'; +import deepcopy from 'deepcopy'; const nestedHandler = { updateParent(key, value) { const levels = this._path.split('.'); levels.push(key); const topLevel = levels[0]; levels.shift(); - const copiedParent = Array.isArray(this._parent[topLevel]) ? [...this._parent[topLevel]] : {...this._parent[topLevel]}; + const copiedParent = Array.isArray(this._parent[topLevel]) + ? [...this._parent[topLevel]] + : { ...this._parent[topLevel] }; const scope = deepcopy(copiedParent); let target = scope; const max_level = levels.length - 1; - for (let i = 0; i < levels.length; i++) { - const level = levels[i]; + levels.some((level, i) => { if (typeof level === 'undefined') { - break; + return true; } if (i === max_level) { if (value == null) { @@ -21,23 +22,27 @@ const nestedHandler = { target[level] = value; } } else { - const obj = target[level] || {}; + const obj = target[level] || (this._array ? [] : {}); target = obj; } - } + }); this._parent[topLevel] = scope; }, get(target, key, receiver) { const reflector = Reflect.get(target, key, receiver); const prop = target[key]; if ( - Object.prototype.toString.call(prop) === '[object Object]' && - prop?.constructor?.name === 'Object' + Array.isArray(prop) || + (Object.prototype.toString.call(prop) === '[object Object]' && + prop?.constructor?.name === 'Object') ) { const thisHandler = { ...nestedHandler }; thisHandler._path = `${this._path}.${key}`; thisHandler._parent = this._parent; - return new Proxy({ ...prop }, thisHandler); + const isArray = Array.isArray(prop); + thisHandler._array = isArray; + const copied = isArray ? [...prop] : { ...prop }; + return new Proxy(copied, thisHandler); } return reflector; }, From 9de2ac569265aefc0ae8aa61c275894b16f95eef Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 15:02:42 +1000 Subject: [PATCH 48/48] Update proxy.js --- src/proxy.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/proxy.js b/src/proxy.js index aa1932645..3257681c2 100644 --- a/src/proxy.js +++ b/src/proxy.js @@ -5,10 +5,7 @@ const nestedHandler = { levels.push(key); const topLevel = levels[0]; levels.shift(); - const copiedParent = Array.isArray(this._parent[topLevel]) - ? [...this._parent[topLevel]] - : { ...this._parent[topLevel] }; - const scope = deepcopy(copiedParent); + const scope = deepcopy(this._parent[topLevel]); let target = scope; const max_level = levels.length - 1; levels.some((level, i) => { @@ -36,13 +33,12 @@ const nestedHandler = { (Object.prototype.toString.call(prop) === '[object Object]' && prop?.constructor?.name === 'Object') ) { - const thisHandler = { ...nestedHandler }; + const thisHandler = deepcopy(nestedHandler); thisHandler._path = `${this._path}.${key}`; thisHandler._parent = this._parent; const isArray = Array.isArray(prop); thisHandler._array = isArray; - const copied = isArray ? [...prop] : { ...prop }; - return new Proxy(copied, thisHandler); + return new Proxy(deepcopy(prop), thisHandler); } return reflector; }, @@ -64,10 +60,10 @@ const proxyHandler = { Object.prototype.toString.call(getValue) === '[object Object]' && getValue?.constructor?.name === 'Object' ) { - const thisHandler = { ...nestedHandler }; + const thisHandler = deepcopy(nestedHandler); thisHandler._path = key; thisHandler._parent = receiver; - return new Proxy({ ...getValue }, thisHandler); + return new Proxy(deepcopy(getValue), thisHandler); } return getValue; },