diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba4f42830274ba4898c5cdc44be7def61cc2ad0..19e4e61f1c3745b49c504ec6ecedc11ea9a2a471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,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 unsaved pointers and keys (dblythy) [#7310](https://github.com/parse-community/parse-server/pull/7310) - Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496) - Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 07d94a366f2342cd9d8a740c19a12a8758be9634..adace3107826f7a294fb7d2b5fe9b5c963e79ef9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2391,6 +2391,53 @@ describe('afterFind hooks', () => { }); }); + it('can set a pointer object in afterFind', async () => { + 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); + objects[0].set('xyz', 'yolo'); + expect(objects[0].get('Pointer').get('foo')).toBe('bar'); + }); + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + const obj2 = await query.first(); + expect(obj2.get('xyz')).toBe('yolo'); + const pointer = obj2.get('Pointer'); + 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(); + 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 => { Parse.Cloud.afterFind('MyObject', req => { expect(req.headers).toBeDefined(); diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 23934675449875e47a086b2b3fd51920e8fb6db9..ab78a4cfa7bcd7e29fc81cdbcb5e871da9c87eed 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 0091c459c0178ad2fed63e3aaf3e1a286d369e07..1a7f83032706ad70e95ed1833c610a9e5a15ff35 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'; @@ -183,8 +183,7 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - deletedParseObject = res.object.toJSON(); - deletedParseObject.className = className; + deletedParseObject = toJSONwithObjects(res.object, res.object.className || className); } if ( (deletedParseObject.className === '_User' || @@ -337,13 +336,13 @@ class ParseLiveQueryServer { return; } if (res.object && typeof res.object.toJSON === 'function') { - currentParseObject = res.object.toJSON(); - currentParseObject.className = res.object.className || className; + currentParseObject = toJSONwithObjects(res.object, res.object.className || className); } - if (res.original && typeof res.original.toJSON === 'function') { - originalParseObject = res.original.toJSON(); - originalParseObject.className = res.original.className || className; + originalParseObject = toJSONwithObjects( + res.original, + res.original.className || className + ); } if ( (currentParseObject.className === '_User' || diff --git a/src/triggers.js b/src/triggers.js index 4d1cb5fba9dfb98302771678f00c82fc498e948c..8320b5fb748a700b90df1952ef2fb5f844695e8e 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -168,6 +168,27 @@ export function _unregisterAll() { Object.keys(_triggerStore).forEach(appId => delete _triggerStore[appId]); } +export function toJSONwithObjects(object, className) { + if (!object || !object.toJSON) { + return {}; + } + const toJSON = object.toJSON(); + 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; + } + toJSON[key] = val._toFullJSON(); + } + if (className) { + toJSON.className = className; + } + return toJSON; +} + export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw 'Missing ApplicationID'; @@ -323,7 +344,7 @@ export function getResponseObject(request, resolve, reject) { response = request.objects; } response = response.map(object => { - return object.toJSON(); + return toJSONwithObjects(object); }); return resolve(response); } @@ -451,12 +472,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; }); }