Skip to content

Commit 263ca5e

Browse files
flovilmartdrew-gross
authored andcommitted
Adds CloudCode handler for beforeFind (#2715)
* Adds CloudCode handler for beforeFind - Allows cloud code to modify a query before it is run - Works with promises for a safer environment - Supports modifiying the current query - Supports issuing new queries * Adds test for cornercase empty queries from rest * Makes sure restOptions is always definied
1 parent ddb0fb8 commit 263ca5e

File tree

4 files changed

+203
-3
lines changed

4 files changed

+203
-3
lines changed

spec/CloudCode.spec.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,3 +1174,110 @@ it('beforeSave should not affect fetched pointers', done => {
11741174
}
11751175
});
11761176
});
1177+
1178+
describe('beforeFind hooks', () => {
1179+
it('should add beforeFind trigger', (done) => {
1180+
Parse.Cloud.beforeFind('MyObject', (req, res) => {
1181+
let q = req.query;
1182+
expect(q instanceof Parse.Query).toBe(true);
1183+
let jsonQuery = q.toJSON();
1184+
expect(jsonQuery.where.key).toEqual('value');
1185+
expect(jsonQuery.where.some).toEqual({'$gt': 10});
1186+
expect(jsonQuery.include).toEqual('otherKey,otherValue');
1187+
expect(jsonQuery.limit).toEqual(100);
1188+
expect(jsonQuery.skip).toBe(undefined);
1189+
});
1190+
1191+
let query = new Parse.Query('MyObject');
1192+
query.equalTo('key', 'value');
1193+
query.greaterThan('some', 10);
1194+
query.include('otherKey');
1195+
query.include('otherValue');
1196+
query.find().then(() => {
1197+
done();
1198+
});
1199+
});
1200+
1201+
it('should use modify', (done) => {
1202+
Parse.Cloud.beforeFind('MyObject', (req) => {
1203+
let q = req.query;
1204+
q.equalTo('forced', true);
1205+
});
1206+
1207+
let obj0 = new Parse.Object('MyObject');
1208+
obj0.set('forced', false);
1209+
1210+
let obj1 = new Parse.Object('MyObject');
1211+
obj1.set('forced', true);
1212+
Parse.Object.saveAll([obj0, obj1]).then(() => {
1213+
let query = new Parse.Query('MyObject');
1214+
query.equalTo('forced', false);
1215+
query.find().then((results) => {
1216+
expect(results.length).toBe(1);
1217+
let firstResult = results[0];
1218+
expect(firstResult.get('forced')).toBe(true);
1219+
done();
1220+
});
1221+
});
1222+
});
1223+
1224+
it('should use the modified the query', (done) => {
1225+
Parse.Cloud.beforeFind('MyObject', (req) => {
1226+
let q = req.query;
1227+
let otherQuery = new Parse.Query('MyObject');
1228+
otherQuery.equalTo('forced', true);
1229+
return Parse.Query.or(q, otherQuery);
1230+
});
1231+
1232+
let obj0 = new Parse.Object('MyObject');
1233+
obj0.set('forced', false);
1234+
1235+
let obj1 = new Parse.Object('MyObject');
1236+
obj1.set('forced', true);
1237+
Parse.Object.saveAll([obj0, obj1]).then(() => {
1238+
let query = new Parse.Query('MyObject');
1239+
query.equalTo('forced', false);
1240+
query.find().then((results) => {
1241+
expect(results.length).toBe(2);
1242+
done();
1243+
});
1244+
});
1245+
});
1246+
1247+
it('should reject queries', (done) => {
1248+
Parse.Cloud.beforeFind('MyObject', (req) => {
1249+
return Promise.reject('Do not run that query');
1250+
});
1251+
1252+
let query = new Parse.Query('MyObject');
1253+
query.find().then(() => {
1254+
fail('should not succeed');
1255+
done();
1256+
}, (err) => {
1257+
expect(err.code).toBe(1);
1258+
expect(err.message).toEqual('Do not run that query');
1259+
done();
1260+
});
1261+
});
1262+
1263+
it('should handle empty where', (done) => {
1264+
Parse.Cloud.beforeFind('MyObject', (req) => {
1265+
let otherQuery = new Parse.Query('MyObject');
1266+
otherQuery.equalTo('some', true);
1267+
return Parse.Query.or(req.query, otherQuery);
1268+
});
1269+
1270+
rp.get({
1271+
url: 'http://localhost:8378/1/classes/MyObject',
1272+
headers: {
1273+
'X-Parse-Application-Id': Parse.applicationId,
1274+
'X-Parse-REST-API-Key': 'rest',
1275+
},
1276+
}).then((result) => {
1277+
done();
1278+
}, (err) =>  {
1279+
fail(err);
1280+
done();
1281+
});
1282+
});
1283+
})

src/cloud-code/Parse.Cloud.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ ParseCloud.afterDelete = function(parseClass, handler) {
4545
triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId);
4646
};
4747

48+
ParseCloud.beforeFind = function(parseClass, handler) {
49+
var className = getClassName(parseClass);
50+
triggers.addTrigger(triggers.Types.beforeFind, className, handler, Parse.applicationId);
51+
};
52+
4853
ParseCloud._removeAllHooks = () => {
4954
triggers._unregisterAll();
5055
}

src/rest.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ var triggers = require('./triggers');
1717
// Returns a promise for an object with optional keys 'results' and 'count'.
1818
function find(config, auth, className, restWhere, restOptions, clientSDK) {
1919
enforceRoleSecurity('find', className, auth);
20-
let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK);
21-
return query.execute();
20+
return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then((result) => {
21+
restWhere = result.restWhere || restWhere;
22+
restOptions = result.restOptions || restOptions;
23+
let query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK);
24+
return query.execute();
25+
});
2226
}
2327

2428
// get is just like find but only queries an objectId.

src/triggers.js

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ export const Types = {
77
beforeSave: 'beforeSave',
88
afterSave: 'afterSave',
99
beforeDelete: 'beforeDelete',
10-
afterDelete: 'afterDelete'
10+
afterDelete: 'afterDelete',
11+
beforeFind: 'beforeFind'
1112
};
1213

1314
const baseStore = function() {
@@ -154,6 +155,29 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
154155
return request;
155156
}
156157

158+
export function getRequestQueryObject(triggerType, auth, query, config) {
159+
var request = {
160+
triggerName: triggerType,
161+
query: query,
162+
master: false,
163+
log: config.loggerController
164+
};
165+
166+
if (!auth) {
167+
return request;
168+
}
169+
if (auth.isMaster) {
170+
request['master'] = true;
171+
}
172+
if (auth.user) {
173+
request['user'] = auth.user;
174+
}
175+
if (auth.installationId) {
176+
request['installationId'] = auth.installationId;
177+
}
178+
return request;
179+
}
180+
157181
// Creates the response object, and uses the request object to pass data
158182
// The API will call this with REST API formatted objects, this will
159183
// transform them to Parse.Object instances expected by Cloud Code.
@@ -216,6 +240,66 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) {
216240
});
217241
}
218242

243+
export function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) {
244+
let trigger = getTrigger(className, triggerType, config.applicationId);
245+
if (!trigger) {
246+
return Promise.resolve({
247+
restWhere,
248+
restOptions
249+
});
250+
}
251+
252+
let parseQuery = new Parse.Query(className);
253+
if (restWhere) {
254+
parseQuery._where = restWhere;
255+
}
256+
if (restOptions) {
257+
if (restOptions.include && restOptions.include.length > 0) {
258+
parseQuery._include = restOptions.include.split(',');
259+
}
260+
if (restOptions.skip) {
261+
parseQuery._skip = restOptions.skip;
262+
}
263+
if (restOptions.limit) {
264+
parseQuery._limit = restOptions.limit;
265+
}
266+
}
267+
let requestObject = getRequestQueryObject(triggerType, auth, parseQuery, config);
268+
return Promise.resolve().then(() => {
269+
return trigger(requestObject);
270+
}).then((result) => {
271+
let queryResult = parseQuery;
272+
if (result && result instanceof Parse.Query) {
273+
queryResult = result;
274+
}
275+
let jsonQuery = queryResult.toJSON();
276+
if (jsonQuery.where) {
277+
restWhere = jsonQuery.where;
278+
}
279+
if (jsonQuery.limit) {
280+
restOptions = restOptions || {};
281+
restOptions.limit = jsonQuery.limit;
282+
}
283+
if (jsonQuery.skip) {
284+
restOptions = restOptions || {};
285+
restOptions.skip = jsonQuery.skip;
286+
}
287+
if (jsonQuery.include) {
288+
restOptions = restOptions || {};
289+
restOptions.include = jsonQuery.include;
290+
}
291+
return {
292+
restWhere,
293+
restOptions
294+
};
295+
}, (err) => {
296+
if (typeof err === 'string') {
297+
throw new Parse.Error(1, err);
298+
} else {
299+
throw err;
300+
}
301+
});
302+
}
219303

220304
// To be used as part of the promise chain when saving/deleting an object
221305
// Will resolve successfully if no trigger is configured

0 commit comments

Comments
 (0)