diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 59ae534df2..3180352aeb 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -201,6 +201,62 @@ describe('Cloud Code', () => { done(); } }); + it('beforeFind can return object without DB operation', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return new Parse.Object('TestObject', { foo: 'bar' }); + }); + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); + }); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); + }); + + it('beforeFind can return array of objects without DB operation', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); + }); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); + }); + + it('beforeFind can return object for get query without DB operation', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); + }); + const testObj = new Parse.Object('beforeFind'); + await testObj.save(); + const newObj = await new Parse.Query('beforeFind').get(testObj.id); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); + }); + + it('beforeFind can return empty array without DB operation', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return []; + }); + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects.length).toBe(0); + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj).toBeUndefined(); + }); it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { @@ -1496,9 +1552,9 @@ describe('Cloud Code', () => { }); /* - TODO: fix for Postgres - trying to delete a field that doesn't exists doesn't play nice - */ + TODO: fix for Postgres + trying to delete a field that doesn't exists doesn't play nice + */ it_exclude_dbs(['postgres'])( 'should fully delete objects when using `unset` and `set` with beforeSave (regression test for #1840)', done => { @@ -2004,7 +2060,6 @@ describe('Cloud Code', () => { } }); }); - describe('cloud functions', () => { it('Should have request ip', done => { Parse.Cloud.define('myFunction', req => { @@ -2919,55 +2974,61 @@ describe('afterFind hooks', () => { }).toThrow('Only the _Session class is allowed for the afterLogout trigger.'); }); - it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)('should skip afterFind hooks for aggregate', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - const pipeline = [ - { - $group: { _id: {} }, - }, - ]; - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results[0].objectId).toEqual(null); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)( + 'should skip afterFind hooks for aggregate', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + const pipeline = [ + { + $group: { _id: {} }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); - it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)('should skip afterFind hooks for distinct', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - obj.set('score', 10); - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.distinct('score'); - }) - .then(results => { - expect(results[0]).toEqual(10); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)( + 'should skip afterFind hooks for distinct', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + obj.set('score', 10); + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.distinct('score'); + }) + .then(results => { + expect(results[0]).toEqual(10); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); it('should throw error if context header is malformed', async () => { let calledBefore = false; @@ -3033,37 +3094,40 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(false); }); - it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)('should expose context in beforeSave/afterSave via header', async () => { - let calledBefore = false; - let calledAfter = false; - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledBefore = true; - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledAfter = true; - }); - const req = request({ - method: 'POST', - url: 'http://localhost:8378/1/classes/TestObject', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', - }, - body: { - foo: 'bar', - }, - }); - await req; - expect(calledBefore).toBe(true); - expect(calledAfter).toBe(true); - }); + it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)( + 'should expose context in beforeSave/afterSave via header', + async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + }, + }); + await req; + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + } + ); it('should override header context with body context in beforeSave/afterSave', async () => { let calledBefore = false; @@ -3347,20 +3411,23 @@ describe('beforeLogin hook', () => { expect(response).toEqual(error); }); - it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)('should have expected data in request in beforeLogin', async done => { - Parse.Cloud.beforeLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)( + 'should have expected data in request in beforeLogin', + async done => { + Parse.Cloud.beforeLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('tupac', 'shakur'); - await Parse.User.logIn('tupac', 'shakur'); - done(); - }); + await Parse.User.signUp('tupac', 'shakur'); + await Parse.User.logIn('tupac', 'shakur'); + done(); + } + ); it('afterFind should not be triggered when saving an object', async () => { let beforeSaves = 0; @@ -3464,20 +3531,23 @@ describe('afterLogin hook', () => { done(); }); - it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)('should have expected data in request in afterLogin', async done => { - Parse.Cloud.afterLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeDefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)( + 'should have expected data in request in afterLogin', + async done => { + Parse.Cloud.afterLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('testuser', 'p@ssword'); - await Parse.User.logIn('testuser', 'p@ssword'); - done(); - }); + await Parse.User.signUp('testuser', 'p@ssword'); + await Parse.User.logIn('testuser', 'p@ssword'); + done(); + } + ); it('context options should override _context object property when saving a new object', async () => { Parse.Cloud.beforeSave('TestObject', req => { @@ -3502,9 +3572,8 @@ describe('afterLogin hook', () => { 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Cloud-Context': '{"a":"a"}', }, - body: JSON.stringify({_context: { hello: 'world' }}), + body: JSON.stringify({ _context: { hello: 'world' } }), }); - }); it('should have access to context when saving a new object', async () => { @@ -3929,7 +3998,7 @@ describe('saveFile hooks', () => { }); }); -describe('Parse.File hooks', () => { +describe('Parse.File hooks', () => { it('find hooks should run', async () => { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); @@ -4047,68 +4116,77 @@ describe('Parse.File hooks', () => { }); expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`); }); - }); +}); describe('Cloud Config hooks', () => { function testConfig() { return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true }); } - it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)('beforeSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)( + 'beforeSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + expect(count).toBe(1); + } + ); - it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)('beforeSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)( + 'beforeSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); + await testConfig(); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); + } + ); - it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)('beforeSave(Parse.Config) should not change config if nothing is returned', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, () => { - count += 1; - return; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)( + 'beforeSave(Parse.Config) should not change config if nothing is returned', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, () => { + count += 1; + return; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + } + ); it('beforeSave(Parse.Config) throw custom error', async () => { Parse.Cloud.beforeSave(Parse.Config, () => { @@ -4149,60 +4227,69 @@ describe('Cloud Config hooks', () => { } }); - it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)('afterSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)( + 'afterSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); - - it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)('afterSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + expect(count).toBe(1); + } + ); - it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)('afterSave(Parse.Config) should throw error', async () => { - Parse.Cloud.afterSave(Parse.Config, () => { - throw new Parse.Error(400, 'It should fail'); - }); - try { + it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)( + 'afterSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); await testConfig(); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(400); - expect(e.message).toBe('It should fail'); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); } - }); + ); + + it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)( + 'afterSave(Parse.Config) should throw error', + async () => { + Parse.Cloud.afterSave(Parse.Config, () => { + throw new Parse.Error(400, 'It should fail'); + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(400); + expect(e.message).toBe('It should fail'); + } + } + ); }); describe('sendEmail', () => { diff --git a/src/rest.js b/src/rest.js index 1f9dbacb73..3411250e95 100644 --- a/src/rest.js +++ b/src/rest.js @@ -23,11 +23,50 @@ function checkTriggers(className, config, types) { function checkLiveQuery(className, config) { return config.liveQueryController && config.liveQueryController.hasLiveQuery(className); } +async function runFindTriggers( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + isGet +) { + const result = await triggers.maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + context, + isGet + ); + + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + + if (result?.objects) { + const objectsFromBeforeFind = result.objects; + + const afterFindProcessedObjects = await triggers.maybeRunAfterFindTrigger( + triggers.Types.afterFind, + auth, + className, + objectsFromBeforeFind, + config, + new Parse.Query(className).withJSON({ where: restWhere, ...restOptions }), + context + ); + + return { + results: afterFindProcessedObjects, + }; + } -// Returns a promise for an object with optional keys 'results' and 'count'. -const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { const query = await RestQuery({ - method: RestQuery.Method.find, + method: isGet ? RestQuery.Method.get : RestQuery.Method.find, config, auth, className, @@ -35,24 +74,40 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK, restOptions, clientSDK, context, + runBeforeFind: false, }); + return query.execute(); +} + +// Returns a promise for an object with optional keys 'results' and 'count'. +const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { + enforceRoleSecurity('find', className, auth); + return runFindTriggers( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + false + ); }; // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - var restWhere = { objectId }; - const query = await RestQuery({ - method: RestQuery.Method.get, + enforceRoleSecurity('get', className, auth); + return runFindTriggers( config, auth, className, - restWhere, + { objectId }, restOptions, clientSDK, context, - }); - return query.execute(); + true + ); }; // Returns a promise that doesn't resolve to any useful value. diff --git a/src/triggers.js b/src/triggers.js index 2dfbeff7ac..65534f1313 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -182,8 +182,11 @@ export function toJSONwithObjects(object, className) { } toJSON[key] = val._toFullJSON(); } + // Preserve original object's className if no override className is provided if (className) { toJSON.className = className; + } else if (object.className && !toJSON.className) { + toJSON.className = object.className; } return toJSON; } @@ -437,69 +440,91 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, l export function maybeRunAfterFindTrigger( triggerType, auth, - className, - objects, + classNameQuery, + objectsInput, config, query, context ) { return new Promise((resolve, reject) => { - const trigger = getTrigger(className, triggerType, config.applicationId); + const trigger = getTrigger(classNameQuery, triggerType, config.applicationId); + if (!trigger) { - return resolve(); + if (objectsInput && objectsInput.length > 0 && objectsInput[0] instanceof Parse.Object) { + return resolve(objectsInput.map(obj => toJSONwithObjects(obj))); + } + return resolve(objectsInput || []); } + const request = getRequestObject(triggerType, auth, null, null, config, context); - if (query) { + if (query instanceof Parse.Query) { request.query = query; + } else if (typeof query === 'object' && query !== null) { + const parseQueryInstance = new Parse.Query(classNameQuery); + if (query.where) { + parseQueryInstance.withJSON(query); + } else { + parseQueryInstance.withJSON({ where: query }); + } + request.query = parseQueryInstance; + } else { + request.query = new Parse.Query(classNameQuery); } + const { success, error } = getResponseObject( request, - object => { - resolve(object); + processedObjectsJSON => { + resolve(processedObjectsJSON); }, - error => { - reject(error); + errorData => { + reject(errorData); } ); logTriggerSuccessBeforeHook( triggerType, - className, - 'AfterFind', - JSON.stringify(objects), + classNameQuery, + 'AfterFind Input (Pre-Transform)', + JSON.stringify( + objectsInput.map(o => (o instanceof Parse.Object ? o.id + ':' + o.className : o)) + ), auth, config.logLevels.triggerBeforeSuccess ); - request.objects = objects.map(object => { - //setting the class name to transform into parse object - object.className = className; - return Parse.Object.fromJSON(object); + request.objects = objectsInput.map(currentObject => { + if (currentObject instanceof Parse.Object) { + return currentObject; + } + // Preserve the original className if it exists, otherwise use the query className + const originalClassName = currentObject.className || classNameQuery; + const tempObjectWithClassName = { ...currentObject, className: originalClassName }; + return Parse.Object.fromJSON(tempObjectWithClassName); }); return Promise.resolve() .then(() => { - return maybeRunValidator(request, `${triggerType}.${className}`, auth); + return maybeRunValidator(request, `${triggerType}.${classNameQuery}`, auth); }) .then(() => { if (request.skipWithMasterKey) { return request.objects; } - const response = trigger(request); - if (response && typeof response.then === 'function') { - return response.then(results => { + const responseFromTrigger = trigger(request); + if (responseFromTrigger && typeof responseFromTrigger.then === 'function') { + return responseFromTrigger.then(results => { return results; }); } - return response; + return responseFromTrigger; }) .then(success, error); - }).then(results => { + }).then(resultsAsJSON => { logTriggerAfterHook( triggerType, - className, - JSON.stringify(results), + classNameQuery, + JSON.stringify(resultsAsJSON), auth, config.logLevels.triggerAfter ); - return results; + return resultsAsJSON; }); } @@ -607,9 +632,19 @@ export function maybeRunQueryTrigger( restOptions = restOptions || {}; restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; } + let objects = undefined; + if (result instanceof Parse.Object) { + objects = [result]; + } else if ( + Array.isArray(result) && + (!result.length || result.every(obj => obj instanceof Parse.Object)) + ) { + objects = result; + } return { restWhere, restOptions, + objects, }; }, err => { @@ -859,7 +894,9 @@ export function maybeRunTrigger( } return new Promise(function (resolve, reject) { var trigger = getTrigger(parseObject.className, triggerType, config.applicationId); - if (!trigger) { return resolve(); } + if (!trigger) { + return resolve(); + } var request = getRequestObject( triggerType, auth, @@ -1031,12 +1068,26 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) return fileObject; } -export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObject, originalConfigObject, config, context) { +export async function maybeRunGlobalConfigTrigger( + triggerType, + auth, + configObject, + originalConfigObject, + config, + context +) { const GlobalConfigClassName = getClassName(Parse.Config); const configTrigger = getTrigger(GlobalConfigClassName, triggerType, config.applicationId); if (typeof configTrigger === 'function') { try { - const request = getRequestObject(triggerType, auth, configObject, originalConfigObject, config, context); + const request = getRequestObject( + triggerType, + auth, + configObject, + originalConfigObject, + config, + context + ); await maybeRunValidator(request, `${triggerType}.${GlobalConfigClassName}`, auth); if (request.skipWithMasterKey) { return configObject;