diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index b5624cd5..a4afa491 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1174,3 +1174,110 @@ it('beforeSave should not affect fetched pointers', done => { } }); }); + +describe('beforeFind hooks', () => { + it('should add beforeFind trigger', (done) => { + Parse.Cloud.beforeFind('MyObject', (req, res) => { + let q = req.query; + expect(q instanceof Parse.Query).toBe(true); + let jsonQuery = q.toJSON(); + expect(jsonQuery.where.key).toEqual('value'); + expect(jsonQuery.where.some).toEqual({'$gt': 10}); + expect(jsonQuery.include).toEqual('otherKey,otherValue'); + expect(jsonQuery.limit).toEqual(100); + expect(jsonQuery.skip).toBe(undefined); + }); + + let query = new Parse.Query('MyObject'); + query.equalTo('key', 'value'); + query.greaterThan('some', 10); + query.include('otherKey'); + query.include('otherValue'); + query.find().then(() => { + done(); + }); + }); + + it('should use modify', (done) => { + Parse.Cloud.beforeFind('MyObject', (req) => { + let q = req.query; + q.equalTo('forced', true); + }); + + let obj0 = new Parse.Object('MyObject'); + obj0.set('forced', false); + + let obj1 = new Parse.Object('MyObject'); + obj1.set('forced', true); + Parse.Object.saveAll([obj0, obj1]).then(() => { + let query = new Parse.Query('MyObject'); + query.equalTo('forced', false); + query.find().then((results) => { + expect(results.length).toBe(1); + let firstResult = results[0]; + expect(firstResult.get('forced')).toBe(true); + done(); + }); + }); + }); + + it('should use the modified the query', (done) => { + Parse.Cloud.beforeFind('MyObject', (req) => { + let q = req.query; + let otherQuery = new Parse.Query('MyObject'); + otherQuery.equalTo('forced', true); + return Parse.Query.or(q, otherQuery); + }); + + let obj0 = new Parse.Object('MyObject'); + obj0.set('forced', false); + + let obj1 = new Parse.Object('MyObject'); + obj1.set('forced', true); + Parse.Object.saveAll([obj0, obj1]).then(() => { + let query = new Parse.Query('MyObject'); + query.equalTo('forced', false); + query.find().then((results) => { + expect(results.length).toBe(2); + done(); + }); + }); + }); + + it('should reject queries', (done) => { + Parse.Cloud.beforeFind('MyObject', (req) => { + return Promise.reject('Do not run that query'); + }); + + let query = new Parse.Query('MyObject'); + query.find().then(() => { + fail('should not succeed'); + done(); + }, (err) => { + expect(err.code).toBe(1); + expect(err.message).toEqual('Do not run that query'); + done(); + }); + }); + + it('should handle empty where', (done) => { + Parse.Cloud.beforeFind('MyObject', (req) => { + let otherQuery = new Parse.Query('MyObject'); + otherQuery.equalTo('some', true); + return Parse.Query.or(req.query, otherQuery); + }); + + rp.get({ + url: 'http://localhost:8378/1/classes/MyObject', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }).then((result) => { + done(); + }, (err) =>  { + fail(err); + done(); + }); + }); +}) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 57c31d81..58cb44f5 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -45,6 +45,11 @@ ParseCloud.afterDelete = function(parseClass, handler) { triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId); }; +ParseCloud.beforeFind = function(parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.beforeFind, className, handler, Parse.applicationId); +}; + ParseCloud._removeAllHooks = () => { triggers._unregisterAll(); } diff --git a/src/rest.js b/src/rest.js index 345d4398..6a4eb584 100644 --- a/src/rest.js +++ b/src/rest.js @@ -17,8 +17,12 @@ var triggers = require('./triggers'); // Returns a promise for an object with optional keys 'results' and 'count'. function find(config, auth, className, restWhere, restOptions, clientSDK) { enforceRoleSecurity('find', className, auth); - let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); - return query.execute(); + return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then((result) => { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); + return query.execute(); + }); } // get is just like find but only queries an objectId. diff --git a/src/triggers.js b/src/triggers.js index f6e344a1..0bbce382 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -7,7 +7,8 @@ export const Types = { beforeSave: 'beforeSave', afterSave: 'afterSave', beforeDelete: 'beforeDelete', - afterDelete: 'afterDelete' + afterDelete: 'afterDelete', + beforeFind: 'beforeFind' }; const baseStore = function() { @@ -154,6 +155,29 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb return request; } +export function getRequestQueryObject(triggerType, auth, query, config) { + var request = { + triggerName: triggerType, + query: query, + master: false, + log: config.loggerController + }; + + if (!auth) { + return request; + } + if (auth.isMaster) { + request['master'] = true; + } + if (auth.user) { + request['user'] = auth.user; + } + if (auth.installationId) { + request['installationId'] = auth.installationId; + } + return request; +} + // Creates the response object, and uses the request object to pass data // The API will call this with REST API formatted objects, this will // transform them to Parse.Object instances expected by Cloud Code. @@ -216,6 +240,66 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { }); } +export function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) { + let trigger = getTrigger(className, triggerType, config.applicationId); + if (!trigger) { + return Promise.resolve({ + restWhere, + restOptions + }); + } + + let parseQuery = new Parse.Query(className); + if (restWhere) { + parseQuery._where = restWhere; + } + if (restOptions) { + if (restOptions.include && restOptions.include.length > 0) { + parseQuery._include = restOptions.include.split(','); + } + if (restOptions.skip) { + parseQuery._skip = restOptions.skip; + } + if (restOptions.limit) { + parseQuery._limit = restOptions.limit; + } + } + let requestObject = getRequestQueryObject(triggerType, auth, parseQuery, config); + return Promise.resolve().then(() => { + return trigger(requestObject); + }).then((result) => { + let queryResult = parseQuery; + if (result && result instanceof Parse.Query) { + queryResult = result; + } + let jsonQuery = queryResult.toJSON(); + if (jsonQuery.where) { + restWhere = jsonQuery.where; + } + if (jsonQuery.limit) { + restOptions = restOptions || {}; + restOptions.limit = jsonQuery.limit; + } + if (jsonQuery.skip) { + restOptions = restOptions || {}; + restOptions.skip = jsonQuery.skip; + } + if (jsonQuery.include) { + restOptions = restOptions || {}; + restOptions.include = jsonQuery.include; + } + return { + restWhere, + restOptions + }; + }, (err) => { + if (typeof err === 'string') { + throw new Parse.Error(1, err); + } else { + throw err; + } + }); +} // To be used as part of the promise chain when saving/deleting an object // Will resolve successfully if no trigger is configured