Skip to content

Adding support for AfterFind #2962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
130 changes: 129 additions & 1 deletion spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1306,4 +1306,132 @@ describe('beforeFind hooks', () => {
done();
});
});
})

});

describe('afterFind hooks', () => {
it('should add afterFind trigger using get',(done) => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
for(var i = 0 ; i < res.results.length ; i++){
res.results[i].set("secretField","###");
}
});
var obj = new Parse.Object('MyObject');
obj.set('secretField', 'SSID');
obj.save().then(function() {
var query = new Parse.Query('MyObject');
query.get(obj.id).then(function(result) {
expect(result.get('secretField')).toEqual('###');
done();
}, function(error) {
fail(error);
done();
});
}, function(error) {
fail(error);
done();
});
});

it('should add afterFind trigger using find',(done) => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
for(var i = 0 ; i < res.results.length ; i++){
res.results[i].set("secretField","###");
}
});
var obj = new Parse.Object('MyObject');
obj.set('secretField', 'SSID');
obj.save().then(function() {
var query = new Parse.Query('MyObject');
query.equalTo('objectId',obj.id);
query.find().then(function(results) {
expect(results[0].get('secretField')).toEqual('###');
done();
}, function(error) {
fail(error);
done();
});
}, function(error) {
fail(error);
done();
});
});

it('should filter out results',(done) => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
var filteredResults = [];
for(var i = 0 ; i < res.results.length ; i++){
if(res.results[i].get("secretField")==="SSID1") {
filteredResults.push(res.results[i]);
}
}
res.results = filteredResults;
});
var obj0 = new Parse.Object('MyObject');
obj0.set('secretField', 'SSID1');
var obj1 = new Parse.Object('MyObject');
obj1.set('secretField', 'SSID2');
Parse.Object.saveAll([obj0, obj1]).then(function() {
var query = new Parse.Query('MyObject');
query.find().then(function(results) {
expect(results[0].get('secretField')).toEqual('SSID1');
expect(results.length).toEqual(1);
done();
}, function(error) {
fail(error);
done();
});
}, function(error) {
fail(error);
done();
});
});

it('should handle failures',(done) => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
res.error(Parse.Error.SCRIPT_FAILED, "It should fail");
});
var obj = new Parse.Object('MyObject');
obj.set('secretField', 'SSID');
obj.save().then(function() {
var query = new Parse.Query('MyObject');
query.equalTo('objectId',obj.id);
query.find().then(function(results) {
fail("AfterFind should handle response failure correctly");
done();
}, function(error) {
done();
});
}, function(error) {
done();
});
});

it('should also work with promise',(done) => {
Parse.Cloud.afterFind('MyObject', (req, res) => {
let promise = new Parse.Promise();
setTimeout(function(){
for(var i = 0 ; i < res.results.length ; i++){
res.results[i].set("secretField","###");
}
promise.resolve(res.results);
}, 1000);
return promise;
});
var obj = new Parse.Object('MyObject');
obj.set('secretField', 'SSID');
obj.save().then(function() {
var query = new Parse.Query('MyObject');
query.equalTo('objectId',obj.id);
query.find().then(function(results) {
expect(results[0].get('secretField')).toEqual('###');
done();
}, function(error) {
fail(error);
});
}, function(error) {
fail(error);
});
});

});
2 changes: 1 addition & 1 deletion spec/ParseAPI.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,7 @@ it('ensure that if you try to sign up a user with a unique username and email, b
done();
}, (e) => {
expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED);
expect(e.message).toEqual('Invalid function.');
expect(e.message).toEqual('Invalid function: "somethingThatDoesDefinitelyNotExist"');
done();
});
});
Expand Down
23 changes: 23 additions & 0 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

var SchemaController = require('./Controllers/SchemaController');
var Parse = require('parse/node').Parse;
var triggers = require('./triggers');

import { default as FilesController } from './Controllers/FilesController';

Expand Down Expand Up @@ -122,6 +123,8 @@ RestQuery.prototype.execute = function(executeOptions) {
return this.runCount();
}).then(() => {
return this.handleInclude();
}).then(() => {
return this.runAfterFindTrigger();
}).then(() => {
return this.response;
});
Expand Down Expand Up @@ -468,6 +471,23 @@ RestQuery.prototype.handleInclude = function() {
return pathResponse;
};


//Returns a promise of a processed set of results
RestQuery.prototype.runAfterFindTrigger = function() {
if (!this.response) {
return;
}
// Avoid doing any setup for triggers if there is no 'afterFind' trigger for this class.
let hasAfterFindHook = triggers.triggerExists(this.className, triggers.Types.afterFind, this.config.applicationId);
if (!hasAfterFindHook) {
return Promise.resolve();
}
// Run afterFind trigger and set the new results
return triggers.maybeRunAfterFindTrigger(triggers.Types.afterFind, this.auth, this.className,this.response.results, this.config).then((results) => {
this.response.results = results;
});
};

// Adds included values to the response.
// Path is a list of field names.
// Returns a promise for an augmented response.
Expand Down Expand Up @@ -640,4 +660,7 @@ function findObjectWithKey(root, key) {
}
}



Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the lines


module.exports = RestQuery;
2 changes: 1 addition & 1 deletion src/Routers/FunctionsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class FunctionsRouter extends PromiseRouter {
theFunction(request, response);
});
} else {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.');
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
}
}
}
27 changes: 27 additions & 0 deletions src/cli/parse-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ const help = function(){
function startServer(options, callback) {
const app = express();
const api = new ParseServer(options);
const sockets = {};

app.use(options.mountPath, api);

var server = app.listen(options.port, callback);
server.on('connection', initializeConnections);

if (options.startLiveQueryServer || options.liveQueryServerOptions) {
let liveQueryServer = server;
if (options.liveQueryPort) {
Expand All @@ -43,8 +47,31 @@ function startServer(options, callback) {
}
ParseServer.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions);
}

function initializeConnections(socket) {
/* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642)

This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */

const socketId = socket.remoteAddress + ':' + socket.remotePort;
sockets[socketId] = socket;

socket.on('close', () => {
delete sockets[socketId];
});
}

function destroyAliveConnections() {
for (const socketId in sockets) {
try {
sockets[socketId].destroy();
} catch (e) { }
}
}

var handleShutdown = function() {
console.log('Termination signal received. Shutting down.');
destroyAliveConnections();
server.close(function () {
process.exit(0);
});
Expand Down
5 changes: 5 additions & 0 deletions src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ ParseCloud.beforeFind = function(parseClass, handler) {
triggers.addTrigger(triggers.Types.beforeFind, className, handler, Parse.applicationId);
};

ParseCloud.afterFind = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger(triggers.Types.afterFind, className, handler, Parse.applicationId);
};

ParseCloud._removeAllHooks = () => {
triggers._unregisterAll();
}
Expand Down
49 changes: 48 additions & 1 deletion src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export const Types = {
afterSave: 'afterSave',
beforeDelete: 'beforeDelete',
afterDelete: 'afterDelete',
beforeFind: 'beforeFind'
beforeFind: 'beforeFind',
afterFind: 'afterFind'
};

const baseStore = function() {
Expand Down Expand Up @@ -240,6 +241,52 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) {
});
}

export function maybeRunAfterFindTrigger(triggerType, auth, className, results, config) {
return new Promise(function (resolve, reject) {
var trigger = getTrigger(className, triggerType, config.applicationId);
if (!trigger) {
return resolve();
}
var request = getRequestObject(triggerType, auth, null, null, config);
var response = {
error: function(code, message) {
if (!message) {
message = code;
code = Parse.Error.SCRIPT_FAILED;
}
var scriptError = new Parse.Error(code, message);
return reject(scriptError);
}
};
logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(results), auth);
var resultsAsParseObjects = results.map(result => {
//setting the class name to transform into parse object
result.className=className;
return Parse.Object.fromJSON(result);
});
response.results = resultsAsParseObjects;
var triggerPromise = trigger(request, response);
logTriggerAfterHook(triggerType, className, JSON.stringify(modifiedJSONResults), auth);
if (triggerPromise && typeof triggerPromise.then === "function") {
return triggerPromise.then(function(promiseResults){
if(promiseResults) {
var modifiedJSONResults = promiseResults.map(result => {
return result.toJSON();
});
resolve(modifiedJSONResults);
}else{
return reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, "AfterFind expect results to be returned in the promise"));
}
});
} else {
var modifiedJSONResults = response.results.map(result => {
return result.toJSON();
});
return resolve(modifiedJSONResults);
}
});
}

export function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) {
let trigger = getTrigger(className, triggerType, config.applicationId);
if (!trigger) {
Expand Down