diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 59ae534df2..e46eaa4a90 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -201,6 +201,226 @@ 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(); + }); + + const { maybeRunAfterFindTrigger } = require('../lib/triggers'); + + describe('maybeRunAfterFindTrigger - direct function tests', () => { + const testConfig = { + applicationId: 'test', + logLevels: { triggerBeforeSuccess: 'info', triggerAfter: 'info' }, + }; + + it('should convert Parse.Object instances to JSON when no trigger defined', async () => { + const className = 'TestParseObjectDirect_' + Date.now(); + + const parseObj1 = new Parse.Object(className); + parseObj1.set('name', 'test1'); + parseObj1.id = 'obj1'; + + const parseObj2 = new Parse.Object(className); + parseObj2.set('name', 'test2'); + parseObj2.id = 'obj2'; + + const result = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [parseObj1, parseObj2], + testConfig, + null, + {} + ); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + expect(result[0].name).toBe('test1'); + expect(result[1].name).toBe('test2'); + }); + + it('should handle null/undefined objectsInput when no trigger', async () => { + const className = 'TestNullDirect_' + Date.now(); + + const resultNull = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + null, + testConfig, + null, + {} + ); + expect(resultNull).toEqual([]); + + const resultUndefined = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + undefined, + testConfig, + null, + {} + ); + expect(resultUndefined).toEqual([]); + + const resultEmpty = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [], + testConfig, + null, + {} + ); + expect(resultEmpty).toEqual([]); + }); + + it('should handle plain object query with where clause', async () => { + const className = 'TestQueryWhereDirect_' + Date.now(); + let receivedQuery = null; + + Parse.Cloud.afterFind(className, req => { + receivedQuery = req.query; + return req.objects; + }); + + const mockObject = { id: 'test123', className: className, name: 'test' }; + + const result = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [mockObject], + testConfig, + { where: { name: 'test' }, limit: 10 }, + {} + ); + + expect(receivedQuery).toBeInstanceOf(Parse.Query); + expect(result).toBeDefined(); + }); + + it('should handle plain object query without where clause', async () => { + const className = 'TestQueryNoWhereDirect_' + Date.now(); + let receivedQuery = null; + + Parse.Cloud.afterFind(className, req => { + receivedQuery = req.query; + return req.objects; + }); + + const mockObject = { id: 'test456', className: className, name: 'test' }; + + const result = await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [mockObject], + testConfig, + { limit: 5, skip: 0 }, + {} + ); + + expect(receivedQuery).toBeInstanceOf(Parse.Query); + expect(result).toBeDefined(); + }); + + it('should create default query for invalid query parameter', async () => { + const className = 'TestInvalidQueryDirect_' + Date.now(); + let receivedQuery = null; + + Parse.Cloud.afterFind(className, req => { + receivedQuery = req.query; + return req.objects; + }); + + const mockObject = { id: 'test789', className: className, name: 'test' }; + + await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [mockObject], + testConfig, + 'invalid_query_string', + {} + ); + + expect(receivedQuery).toBeInstanceOf(Parse.Query); + expect(receivedQuery.className).toBe(className); + + receivedQuery = null; + + await maybeRunAfterFindTrigger( + 'afterFind', + null, + className, + [mockObject], + testConfig, + null, + {} + ); + + expect(receivedQuery).toBeInstanceOf(Parse.Query); + expect(receivedQuery.className).toBe(className); + }); + }); it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { 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..2bad3db748 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 => {