From 7baf7a057ad1fdb41c604d00266a7cf84f9fcfaa Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 1 Apr 2021 16:53:13 +1100 Subject: [PATCH 1/5] fix: set objects in afterFind triggers --- spec/CloudCode.spec.js | 19 ++++++++++++++ spec/ParseLiveQuery.spec.js | 38 +++++++++++++++++++++++++++ src/LiveQuery/ParseLiveQueryServer.js | 10 +++---- src/triggers.js | 21 ++++++++++++++- 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c53a284273..65e61b256f 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2383,6 +2383,25 @@ describe('afterFind hooks', () => { }); }); + it('can set a pointer object in afterFind', async done => { + const obj = new Parse.Object('MyObject'); + await obj.save(); + Parse.Cloud.afterFind('MyObject', async ({ objects }) => { + const otherObject = new Parse.Object('Test'); + otherObject.set('foo', 'bar'); + await otherObject.save(); + objects[0].set('Pointer', otherObject); + expect(objects[0].get('Pointer').get('foo')).toBe('bar'); + return objects; + }); + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + const [obj2] = await query.find(); + const pointer = obj2.get('Pointer'); + expect(pointer.get('foo')).toBe('bar'); + done(); + }); + it('should have request headers', done => { Parse.Cloud.afterFind('MyObject', req => { expect(req.headers).toBeDefined(); diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 43e91e03bb..61aa76b188 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -358,6 +358,44 @@ describe('ParseLiveQuery', function () { await object.save(); }); + it('can handle afterEvent set pointers', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const object = new TestObject(); + await object.save(); + + const secondObject = new Parse.Object('Test2'); + secondObject.set('foo', 'bar'); + await secondObject.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', async ({ object }) => { + const query = new Parse.Query('Test2'); + const obj = await query.first(); + object.set('obj', obj); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.get('obj')).toBeDefined(); + expect(object.get('obj').get('foo')).toBe('bar'); + done(); + }); + subscription.on('error', () => { + fail('error should not have been called.'); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + it('can handle async afterEvent modification', async done => { await reconfigureServer({ liveQuery: { diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index a4a6e6e777..71c0706d9e 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,7 +10,7 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { runLiveQueryEventHandlers, getTrigger, runTrigger } from '../triggers'; +import { runLiveQueryEventHandlers, getTrigger, runTrigger, toJSONwithObjects } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -181,8 +181,7 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - deletedParseObject = res.object.toJSON(); - deletedParseObject.className = className; + deletedParseObject = toJSONwithObjects(res.object); } client.pushDelete(requestId, deletedParseObject); } catch (error) { @@ -326,12 +325,11 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - currentParseObject = res.object.toJSON(); + currentParseObject = toJSONwithObjects(res.object); currentParseObject.className = res.object.className || className; } - if (res.original && typeof res.original.toJSON === 'function') { - originalParseObject = res.original.toJSON(); + originalParseObject = toJSONwithObjects(res.original); originalParseObject.className = res.original.className || className; } const functionName = diff --git a/src/triggers.js b/src/triggers.js index 0a9e234224..9394ad95a5 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -161,6 +161,25 @@ export function _unregisterAll() { Object.keys(_triggerStore).forEach(appId => delete _triggerStore[appId]); } +export function toJSONwithObjects(object) { + if (!object || !object.toJSON) { + return {}; + } + const toJSON = object.toJSON(); + for (const key of Object.keys(toJSON)) { + const val = toJSON[key]; + if (!val || !val.__type || val.__type !== 'Pointer') { + continue; + } + const pointer = object.get(key); + const json = pointer.toJSON(); + json.className = pointer.className; + json.__type = 'Object'; + toJSON[key] = json; + } + return toJSON; +} + export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw 'Missing ApplicationID'; @@ -316,7 +335,7 @@ export function getResponseObject(request, resolve, reject) { response = request.objects; } response = response.map(object => { - return object.toJSON(); + return toJSONwithObjects(object); }); return resolve(response); } From 335c038a262d3741a5d09bf5155036650e801668 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 4 Apr 2021 22:04:55 +1000 Subject: [PATCH 2/5] changelog --- CHANGELOG.md | 1 + src/LiveQuery/ParseLiveQueryServer.js | 11 ++++++----- src/triggers.js | 5 ++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b6accab4..8db3c7ce3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ ___ - EXPERIMENTAL: Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification (Manuel Trezza) [#6891](https://github.com/parse-community/parse-server/issues/6891) - EXPERIMENTAL: Added custom routes to easily customize flows for password reset, email verification or build entirely new flows (Manuel Trezza) [#7231](https://github.com/parse-community/parse-server/issues/7231) ### Other Changes +- Allow afterFind and afterLiveQueryEvent to set pointers (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310) - Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196) - request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078) - Winston Logger interpolating stdout to console (dplewis) [#7114](https://github.com/parse-community/parse-server/pull/7114) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 71c0706d9e..b715817721 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -181,7 +181,7 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - deletedParseObject = toJSONwithObjects(res.object); + deletedParseObject = toJSONwithObjects(res.object, res.object.className || className); } client.pushDelete(requestId, deletedParseObject); } catch (error) { @@ -325,12 +325,13 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - currentParseObject = toJSONwithObjects(res.object); - currentParseObject.className = res.object.className || className; + currentParseObject = toJSONwithObjects(res.object, res.object.className || className); } if (res.original && typeof res.original.toJSON === 'function') { - originalParseObject = toJSONwithObjects(res.original); - originalParseObject.className = res.original.className || className; + originalParseObject = toJSONwithObjects( + res.original, + res.original.className || className + ); } const functionName = 'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1); diff --git a/src/triggers.js b/src/triggers.js index 9394ad95a5..42dd0faefa 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -161,7 +161,7 @@ export function _unregisterAll() { Object.keys(_triggerStore).forEach(appId => delete _triggerStore[appId]); } -export function toJSONwithObjects(object) { +export function toJSONwithObjects(object, className) { if (!object || !object.toJSON) { return {}; } @@ -177,6 +177,9 @@ export function toJSONwithObjects(object) { json.__type = 'Object'; toJSON[key] = json; } + if (className) { + toJSON.className = className; + } return toJSON; } From cd69b01439c6f125347383b5fbf2f7903ff11e3c Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Sep 2021 16:40:22 +1000 Subject: [PATCH 3/5] add support for other keys --- CHANGELOG.md | 10 +++++----- spec/CloudCode.spec.js | 23 ++++++++++++++++++++--- src/triggers.js | 15 +++++++-------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3dcbfcfc..cb17149bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -151,7 +151,7 @@ ___ - Refactor: uniform issue templates across repos (Manuel Trezza) [#7528](https://github.com/parse-community/parse-server/pull/7528) - ci: bump ci environment (Manuel Trezza) [#7539](https://github.com/parse-community/parse-server/pull/7539) - CI now pushes docker images to Docker Hub (Corey Baker) [#7548](https://github.com/parse-community/parse-server/pull/7548) -- Allow afterFind and afterLiveQueryEvent to set pointers (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310) +- Allow afterFind and afterLiveQueryEvent to set unsaved pointers and keys (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310) ## 4.10.3 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.2...4.10.3) @@ -178,15 +178,15 @@ ___ *Versions >4.5.2 and <4.10.0 are skipped.* -> ⚠️ A security incident caused a number of incorrect version tags to be pushed to the Parse Server repository. These version tags linked to a personal fork of a contributor who had write access to the repository. The code to which these tags linked has not been reviewed or approved by Parse Platform. Even though no releases were published with these incorrect versions, it was possible to define a Parse Server dependency that pointed to these version tags, for example if you defined this dependency: +> ⚠️ A security incident caused a number of incorrect version tags to be pushed to the Parse Server repository. These version tags linked to a personal fork of a contributor who had write access to the repository. The code to which these tags linked has not been reviewed or approved by Parse Platform. Even though no releases were published with these incorrect versions, it was possible to define a Parse Server dependency that pointed to these version tags, for example if you defined this dependency: > ```js > "parse-server": "git@github.com:parse-community/parse-server.git#4.9.3" > ``` -> +> > We have since deleted the incorrect version tags, but they may still show up if your personal fork on GitHub or locally. We do not know when these tags have been pushed to the Parse Server repository, but we first became aware of this issue on July 21, 2021. We are not aware of any malicious code or concerns related to privacy, security or legality (e.g. proprietary code). However, it has been reported that some functionality does not work as expected and the introduction of security vulnerabilities cannot be ruled out. > -> You may be also affected if you used the Bitnami image for Parse Server. Bitnami picked up the incorrect version tag `4.9.3` and published a new Bitnami image for Parse Server. -> +> You may be also affected if you used the Bitnami image for Parse Server. Bitnami picked up the incorrect version tag `4.9.3` and published a new Bitnami image for Parse Server. +> >**If you are using any of the affected versions, we urgently recommend to upgrade to version `4.10.0`.** ## 4.5.2 diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2f9b1c7e11..c4a268cdc6 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2383,7 +2383,7 @@ describe('afterFind hooks', () => { }); }); - it('can set a pointer object in afterFind', async done => { + it('can set a pointer object in afterFind', async () => { const obj = new Parse.Object('MyObject'); await obj.save(); Parse.Cloud.afterFind('MyObject', async ({ objects }) => { @@ -2391,15 +2391,32 @@ describe('afterFind hooks', () => { otherObject.set('foo', 'bar'); await otherObject.save(); objects[0].set('Pointer', otherObject); + objects[0].set('xyz', 'yolo'); expect(objects[0].get('Pointer').get('foo')).toBe('bar'); return objects; }); const query = new Parse.Query('MyObject'); query.equalTo('objectId', obj.id); - const [obj2] = await query.find(); + const [obj2] = await query.first(); + expect(obj2.get('xyz')).toBe('yolo'); const pointer = obj2.get('Pointer'); expect(pointer.get('foo')).toBe('bar'); - done(); + }); + + it('can return a unsaved object in afterFind', async () => { + const obj = new Parse.Object('MyObject'); + await obj.save(); + Parse.Cloud.afterFind('MyObject', async () => { + const otherObject = new Parse.Object('Test'); + otherObject.set('foo', 'bar'); + return [otherObject]; + }); + const query = new Parse.Query('MyObject'); + const obj2 = await query.first(); + expect(obj2.get('foo')).toEqual('bar'); + expect(obj2.id).toBeUndefined(); + await obj2.save(); + expect(obj2.id).toBeDefined(); }); it('should have request headers', done => { diff --git a/src/triggers.js b/src/triggers.js index 71f2e020fb..370d7d2af5 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -166,16 +166,15 @@ export function toJSONwithObjects(object, className) { return {}; } const toJSON = object.toJSON(); - for (const key of Object.keys(toJSON)) { - const val = toJSON[key]; - if (!val || !val.__type || val.__type !== 'Pointer') { + const stateController = Parse.CoreManager.getObjectStateController(); + const [pending] = stateController.getPendingOps(object._getStateIdentifier()); + for (const key in pending) { + const val = object.get(key); + if (!val || !val._toFullJSON) { + toJSON[key] = val; continue; } - const pointer = object.get(key); - const json = pointer.toJSON(); - json.className = pointer.className; - json.__type = 'Object'; - toJSON[key] = json; + toJSON[key] = val._toFullJSON(); } if (className) { toJSON.className = className; From 2947d0e3d219a4ab6f46d37e9e7cac1938480b6a Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Sep 2021 16:55:31 +1000 Subject: [PATCH 4/5] fix intermediate --- spec/CloudCode.spec.js | 3 +-- src/triggers.js | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c4a268cdc6..68a5092f18 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2393,11 +2393,10 @@ describe('afterFind hooks', () => { objects[0].set('Pointer', otherObject); objects[0].set('xyz', 'yolo'); expect(objects[0].get('Pointer').get('foo')).toBe('bar'); - return objects; }); const query = new Parse.Query('MyObject'); query.equalTo('objectId', obj.id); - const [obj2] = await query.first(); + const obj2 = await query.first(); expect(obj2.get('xyz')).toBe('yolo'); const pointer = obj2.get('Pointer'); expect(pointer.get('foo')).toBe('bar'); diff --git a/src/triggers.js b/src/triggers.js index 370d7d2af5..e56b11d8a6 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -465,12 +465,6 @@ export function maybeRunAfterFindTrigger( const response = trigger(request); if (response && typeof response.then === 'function') { return response.then(results => { - if (!results) { - throw new Parse.Error( - Parse.Error.SCRIPT_FAILED, - 'AfterFind expect results to be returned in the promise' - ); - } return results; }); } From 785574083cce02e8f3d37c37abdb4be91a1dc3f8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 8 Oct 2021 11:34:11 +1100 Subject: [PATCH 5/5] increase coverage --- spec/CloudCode.spec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 8112ab2007..adace31078 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2410,6 +2410,18 @@ describe('afterFind hooks', () => { expect(pointer.get('foo')).toBe('bar'); }); + it('can set invalid object in afterFind', async () => { + const obj = new Parse.Object('MyObject'); + await obj.save(); + Parse.Cloud.afterFind('MyObject', () => [{}]); + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + const obj2 = await query.first(); + expect(obj2).toBeDefined(); + expect(obj2.toJSON()).toEqual({}); + expect(obj2.id).toBeUndefined(); + }); + it('can return a unsaved object in afterFind', async () => { const obj = new Parse.Object('MyObject'); await obj.save();