Adds Hooks API

Adds Parse.Hooks.js in src/cloud-code/Parse.Hooks.js
Moves Cloud code related functions in src/cloud-code
This commit is contained in:
Florent Vilmart
2016-02-05 14:38:09 -05:00
parent a7262dafd8
commit 9ac7a52e40
19 changed files with 1121 additions and 104 deletions

6
.gitignore vendored
View File

@@ -32,5 +32,11 @@ node_modules
# WebStorm/IntelliJ # WebStorm/IntelliJ
.idea .idea
# visual studio code
.vscode
# Babel.js # Babel.js
lib/ lib/
# cache folder
.cache

View File

@@ -1,4 +1,4 @@
var httpRequest = require("../src/httpRequest"), var httpRequest = require("../src/cloud-code/httpRequest"),
bodyParser = require('body-parser'), bodyParser = require('body-parser'),
express = require("express"); express = require("express");

View File

@@ -786,7 +786,11 @@ describe('Parse.ACL', () => {
equal(results.length, 1); equal(results.length, 1);
var result = results[0]; var result = results[0];
ok(result); ok(result);
if (!result) {
fail("should have result");
} else {
equal(result.id, object.id); equal(result.id, object.id);
}
done(); done();
} }
}); });

View File

@@ -275,7 +275,11 @@ describe('miscellaneous', function() {
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
return objAgain.fetch(); return objAgain.fetch();
}).then((objAgain) => { }).then((objAgain) => {
if (objAgain) {
expect(objAgain.get('foo')).toEqual('bar'); expect(objAgain.get('foo')).toEqual('bar');
} else {
fail("unable to fetch the object ", id);
}
done(); done();
}, (error) => { }, (error) => {
// We should have been able to fetch the object again // We should have been able to fetch the object again
@@ -351,6 +355,11 @@ describe('miscellaneous', function() {
it('test cloud function return types', function(done) { it('test cloud function return types', function(done) {
Parse.Cloud.run('foo').then((result) => { Parse.Cloud.run('foo').then((result) => {
expect(result.object instanceof Parse.Object).toBeTruthy(); expect(result.object instanceof Parse.Object).toBeTruthy();
if (!result.object) {
fail("Unable to run foo");
done();
return;
}
expect(result.object.className).toEqual('Foo'); expect(result.object.className).toEqual('Foo');
expect(result.object.get('x')).toEqual(2); expect(result.object.get('x')).toEqual(2);
var bar = result.object.get('relation'); var bar = result.object.get('relation');
@@ -381,19 +390,21 @@ describe('miscellaneous', function() {
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
expect(results[0]['foo']).toEqual('bar'); expect(results[0]['foo']).toEqual('bar');
done(); done();
}); }).fail( err => {
fail(err);
done();
})
}); });
describe('beforeSave', () => { describe('beforeSave', () => {
beforeEach(done => { beforeEach(done => {
// Make sure the required mock for all tests is unset. // Make sure the required mock for all tests is unset.
delete Parse.Cloud.Triggers.beforeSave.GameScore; Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done(); done();
}); });
afterEach(done => { afterEach(done => {
// Make sure the required mock for all tests is unset. // Make sure the required mock for all tests is unset.
delete Parse.Cloud.Triggers.beforeSave.GameScore; Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done(); done();
}); });
@@ -610,8 +621,8 @@ describe('miscellaneous', function() {
}).then(function() { }).then(function() {
// Make sure the checking has been triggered // Make sure the checking has been triggered
expect(triggerTime).toBe(2); expect(triggerTime).toBe(2);
// Clear mock afterSave // Clear mock beforeSave
delete Parse.Cloud.Triggers.afterSave.GameScore; Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore");
done(); done();
}, function(error) { }, function(error) {
fail(error); fail(error);
@@ -663,9 +674,10 @@ describe('miscellaneous', function() {
// Make sure the checking has been triggered // Make sure the checking has been triggered
expect(triggerTime).toBe(2); expect(triggerTime).toBe(2);
// Clear mock afterSave // Clear mock afterSave
delete Parse.Cloud.Triggers.afterSave.GameScore; Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore");
done(); done();
}, function(error) { }, function(error) {
console.error(error);
fail(error); fail(error);
done(); done();
}); });
@@ -678,12 +690,12 @@ describe('miscellaneous', function() {
}); });
Parse.Cloud.run('willFail').then((s) => { Parse.Cloud.run('willFail').then((s) => {
fail('Should not have succeeded.'); fail('Should not have succeeded.');
delete Parse.Cloud.Functions['willFail']; Parse.Cloud._removeHook("Functions", "willFail");
done(); done();
}, (e) => { }, (e) => {
expect(e.code).toEqual(141); expect(e.code).toEqual(141);
expect(e.message).toEqual('noway'); expect(e.message).toEqual('noway');
delete Parse.Cloud.Functions['willFail']; Parse.Cloud._removeHook("Functions", "willFail");
done(); done();
}); });
}); });
@@ -712,7 +724,7 @@ describe('miscellaneous', function() {
// Make sure query string params override body params // Make sure query string params override body params
expect(res.other).toEqual('2'); expect(res.other).toEqual('2');
expect(res.foo).toEqual("bar"); expect(res.foo).toEqual("bar");
delete Parse.Cloud.Functions['echoParams']; Parse.Cloud._removeHook("Functions",'echoParams');
done(); done();
}); });
}); });
@@ -726,7 +738,7 @@ describe('miscellaneous', function() {
}); });
Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => {
delete Parse.Cloud.Functions['functionWithParameterValidation']; Parse.Cloud._removeHook("Functions", "functionWithParameterValidation");
done(); done();
}, (e) => { }, (e) => {
fail('Validation should not have failed.'); fail('Validation should not have failed.');
@@ -744,7 +756,7 @@ describe('miscellaneous', function() {
Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => { Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => {
fail('Validation should not have succeeded'); fail('Validation should not have succeeded');
delete Parse.Cloud.Functions['functionWithParameterValidationFailure']; Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure");
done(); done();
}, (e) => { }, (e) => {
expect(e.code).toEqual(141); expect(e.code).toEqual(141);

390
spec/ParseHooks.spec.js Normal file
View File

@@ -0,0 +1,390 @@
/* global describe, it, expect, fail, Parse */
var request = require('request');
var triggers = require('../src/triggers');
var HooksController = require('../src/Controllers/HooksController').default;
var express = require("express");
var bodyParser = require('body-parser');
// Inject the hooks API
Parse.Hooks = require("../src/cloud-code/Parse.Hooks");
var port = 12345;
var hookServerURL = "http://localhost:"+port;
var app = express();
app.use(bodyParser.json({ 'type': '*/*' }))
app.listen(12345);
describe('Hooks', () => {
it("should have some hooks registered", (done) => {
Parse.Hooks.getFunctions().then((res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
fail(err);
done();
});
});
it("should have some triggers registered", (done) => {
Parse.Hooks.getTriggers().then( (res) => {
expect(res.constructor).toBe(Array.prototype.constructor);
done();
}, (err) => {
fail(err);
done();
});
});
it("should CRUD a function registration", (done) => {
// Create
Parse.Hooks.createFunction("My-Test-Function", "http://someurl").then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://someurl")
// Find
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateFunction("My-Test-Function", "http://anotherurl");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res.functionName).toBe("My-Test-Function");
expect(res.url).toBe("http://anotherurl")
return Parse.Hooks.deleteFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getFunction("My-Test-Function");
}, (err) => {
fail(err);
done();
}).then((res) => {
fail("Should not succeed")
done();
}, (err) => {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.error).toBe("no function named: My-Test-Function is defined")
done();
})
});
it("should CRUD a trigger registration", (done) => {
// Create
Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => {
expect(res.className).toBe("MyClass");
expect(res.triggerName).toBe("beforeDelete");
expect(res.url).toBe("http://someurl")
// Find
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res).not.toBe(null);
expect(res).not.toBe(undefined);
expect(res.url).toBe("http://someurl");
// delete
return Parse.Hooks.updateTrigger("MyClass","beforeDelete", "http://anotherurl");
}, (err) => {
fail(err);
done();
}).then((res) => {
expect(res.className).toBe("MyClass");
expect(res.url).toBe("http://anotherurl")
return Parse.Hooks.deleteTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then((res) => {
// Find again! but should be deleted
return Parse.Hooks.getTrigger("MyClass","beforeDelete");
}, (err) => {
fail(err);
done();
}).then(function(){
fail("should not succeed");
done();
}, (err) => {
expect(err).not.toBe(null);
expect(err).not.toBe(undefined);
expect(err.code).toBe(143);
expect(err.error).toBe("class MyClass does not exist")
done();
});
});
it("should fail to register hooks without Master Key", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-REST-API-Key": Parse.restKey,
},
body: JSON.stringify({ url: "http://hello.word", functionName: "SomeFunction"})
}, (err, res, body) => {
body = JSON.parse(body);
expect(body.error).toBe("unauthorized");
done();
})
});
it("should fail trying to create two times the same function", (done) => {
Parse.Hooks.createFunction("my_new_function", "http://url.com").then( () => {
return Parse.Hooks.createFunction("my_new_function", "http://url.com")
}, () => {
fail("should create a new function");
}).then( () => {
fail("should not be able to create the same function");
}, (err) => {
expect(err).not.toBe(undefined);
expect(err).not.toBe(null);
expect(err.code).toBe(143);
expect(err.error).toBe('function name: my_new_function already exits')
return Parse.Hooks.deleteFunction("my_new_function");
}).then(() => {
done();
}, (err) => {
fail(err);
done();
})
});
it("should fail trying to create two times the same trigger", (done) => {
Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com").then( () => {
return Parse.Hooks.createTrigger("MyClass", "beforeSave", "http://url.com")
}, () => {
fail("should create a new trigger");
}).then( () => {
fail("should not be able to create the same trigger");
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class MyClass already has trigger beforeSave')
return Parse.Hooks.deleteTrigger("MyClass", "beforeSave");
}).then(() => {
done();
}, (err) => {
fail(err);
done();
})
});
it("should fail trying to update a function that don't exist", (done) => {
Parse.Hooks.updateFunction("A_COOL_FUNCTION", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
return Parse.Hooks.getFunction("A_COOL_FUNCTION")
}).then( (res) => {
fail("the function should not exist");
done();
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('no function named: A_COOL_FUNCTION is defined');
done();
});
});
it("should fail trying to update a trigger that don't exist", (done) => {
Parse.Hooks.updateTrigger("AClassName","beforeSave", "http://url.com").then( () => {
fail("Should not succeed")
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class AClassName does not exist');
return Parse.Hooks.getTrigger("AClassName","beforeSave")
}).then( (res) => {
fail("the function should not exist");
done();
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe('class AClassName does not exist');
done();
});
});
it("should fail trying to create a malformed function", (done) => {
Parse.Hooks.createFunction("MyFunction").then( (res) => {
fail(res);
}, (err) => {
expect(err.code).toBe(143);
expect(err.error).toBe("invalid hook declaration");
done();
});
});
it("should fail trying to create a malformed function (REST)", (done) => {
request.post(Parse.serverURL+"/hooks/functions", {
headers: {
"X-Parse-Application-Id": Parse.applicationId,
"X-Parse-Master-Key": Parse.masterKey,
},
body: JSON.stringify({ functionName: "SomeFunction"})
}, (err, res, body) => {
body = JSON.parse(body);
expect(body.error).toBe("invalid hook declaration");
expect(body.code).toBe(143);
done();
})
});
it("should create hooks and properly preload them", (done) => {
var promises = [];
for (var i = 0; i<5; i++) {
promises.push(Parse.Hooks.createTrigger("MyClass"+i, "beforeSave", "http://url.com/beforeSave/"+i));
promises.push(Parse.Hooks.createFunction("AFunction"+i, "http://url.com/function"+i));
}
Parse.Promise.when(promises).then(function(results){
for (var i=0; i<5; i++) {
// Delete everything from memory, as the server just started
triggers.removeTrigger("beforeSave", "MyClass"+i, Parse.applicationId);
triggers.removeFunction("AFunction"+i, Parse.applicationId);
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined();
}
const hooksController = new HooksController(Parse.applicationId);
return hooksController.load()
}, (err) => {
console.error(err);
fail();
done();
}).then(function() {
for (var i=0; i<5; i++) {
expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).not.toBeUndefined();
expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).not.toBeUndefined();
}
done();
}, (err) => {
console.error(err);
fail();
done();
})
});
it("should run the function on the test server", (done) => {
app.post("/SomeFunction", function(req, res) {
res.json({success:"OK!"});
});
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunction").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
expect(res).toBe("OK!");
done();
}, (err) => {
console.error(err);
fail("Should not fail calling a function");
done();
})
});
it("should run the function on the test server", (done) => {
app.post("/SomeFunctionError", function(req, res) {
res.json({error: {code: 1337, error: "hacking that one!"}});
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/SomeFunctionError").then(function(){
return Parse.Cloud.run("SOME_TEST_FUNCTION")
}, (err) => {
console.error(err);
fail("Should not fail creating a function");
done();
}).then(function(res){
fail("Should not succeed calling that function");
done();
}, (err) => {
expect(err.code).toBe(141);
expect(err.message.code).toEqual(1337)
expect(err.message.error).toEqual("hacking that one!");
done();
});
});
it("should run the beforeSave hook on the test server", (done) => {
var triggerCount = 0;
app.post("/BeforeSaveSome", function(req, res) {
triggerCount++;
var object = req.body.object;
object.hello = "world";
// Would need parse cloud express to set much more
// But this should override the key upon return
res.json({success: {object: object}});
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave" ,hookServerURL+"/BeforeSaveSome").then(function(){
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function(res){
expect(triggerCount).toBe(1);
return res.fetch();
}).then(function(res){
expect(res.get("hello")).toEqual("world");
done();
}).fail((err) => {
console.error(err);
fail("Should not fail creating a function");
done();
});
});
it("should run the afterSave hook on the test server", (done) => {
var triggerCount = 0;
var newObjectId;
app.post("/AfterSaveSome", function(req, res) {
triggerCount++;
var obj = new Parse.Object("AnotherObject");
obj.set("foo", "bar");
obj.save().then(function(obj){
newObjectId = obj.id;
res.json({success: {}});
})
});
// The function is delete as the DB is dropped between calls
Parse.Hooks.createTrigger("SomeRandomObject", "afterSave" ,hookServerURL+"/AfterSaveSome").then(function(){
const obj = new Parse.Object("SomeRandomObject");
return obj.save();
}).then(function(res){
var promise = new Parse.Promise();
// Wait a bit here as it's an after save
setTimeout(function(){
expect(triggerCount).toBe(1);
var q = new Parse.Query("AnotherObject");
q.get(newObjectId).then(function(r){
promise.resolve(r);
});
}, 300)
return promise;
}).then(function(res){
expect(res.get("foo")).toEqual("bar");
done();
}).fail((err) => {
console.error(err);
fail("Should not fail creating a function");
done();
});
});
});

View File

@@ -1,5 +1,3 @@
var Parse = require('parse/node').Parse;
Parse.Cloud.define('hello', function(req, res) { Parse.Cloud.define('hello', function(req, res) {
res.success('Hello world!'); res.success('Hello world!');
}); });

View File

@@ -1,6 +1,7 @@
// A Config object provides information about how a specific app is // A Config object provides information about how a specific app is
// configured. // configured.
// mount is the URL for the root of the API; includes http, domain, etc. // mount is the URL for the root of the API; includes http, domain, etc.
export class Config { export class Config {
constructor(applicationId, mount) { constructor(applicationId, mount) {
@@ -23,8 +24,8 @@ export class Config {
this.fileKey = cacheInfo.fileKey; this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds; this.facebookAppIds = cacheInfo.facebookAppIds;
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId); this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
this.hooksController = cacheInfo.hooksController;
this.filesController = cacheInfo.filesController; this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController; this.pushController = cacheInfo.pushController;
this.loggerController = cacheInfo.loggerController; this.loggerController = cacheInfo.loggerController;

View File

@@ -0,0 +1,229 @@
var DatabaseAdapter = require('../DatabaseAdapter'),
triggers = require('../triggers');
const collection = "_Hooks";
export class HooksController {
constructor(applicationId) {
this.applicationId = applicationId;
}
database() {
return DatabaseAdapter.getDatabaseConnection(this.applicationId);
}
collection() {
if (this._collection) {
return Promise.resolve(this._collection)
}
return this.database().rawCollection(collection).then((collection) => {
this._collection = collection;
return collection;
});
}
getFunction(functionName) {
return this.getOne({functionName: functionName})
}
getFunctions() {
return this.get({functionName: { $exists: true }})
}
getTrigger(className, triggerName) {
return this.getOne({className: className, triggerName: triggerName })
}
getTriggers() {
return this.get({className: { $exists: true }, triggerName: { $exists: true }})
}
deleteFunction(functionName) {
triggers.removeFunction(functionName, this.applicationId);
return this.delete({functionName: functionName});
}
deleteTrigger(className, triggerName) {
triggers.removeTrigger(triggerName, className, this.applicationId);
return this.delete({className: className, triggerName: triggerName});
}
delete(query) {
return this.collection().then((collection) => {
return collection.remove(query)
}).then( (res) => {
return {};
}, 1);
}
getOne(query) {
return this.collection()
.then(coll => coll.findOne(query, {_id: 0}))
.then(hook => {
return hook;
});
}
get(query) {
return this.collection()
.then(coll => coll.find(query, {_id: 0}).toArray())
.then(hooks => {
return hooks;
});
}
getHooks() {
return this.collection()
.then(coll => coll.find({}, {_id: 0}).toArray())
.then(hooks => {
return hooks;
}, () => ([]))
}
saveHook(hook) {
var query;
if (hook.functionName && hook.url) {
query = {functionName: hook.functionName }
} else if (hook.triggerName && hook.className && hook.url) {
query = { className: hook.className, triggerName: hook.triggerName }
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.collection().then((collection) => {
return collection.update(query, hook, {upsert: true})
}).then(function(res){
return hook;
})
}
addHookToTriggers(hook) {
var wrappedFunction = wrapToHTTPRequest(hook);
wrappedFunction.url = hook.url;
if (hook.className) {
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this.applicationId)
} else {
triggers.addFunction(hook.functionName, wrappedFunction, null, this.applicationId);
}
}
addHook(hook) {
this.addHookToTriggers(hook);
return this.saveHook(hook);
}
createOrUpdateHook(aHook) {
var hook;
if (aHook && aHook.functionName && aHook.url) {
hook = {};
hook.functionName = aHook.functionName;
hook.url = aHook.url;
} else if (aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName]) {
hook = {};
hook.className = aHook.className;
hook.url = aHook.url;
hook.triggerName = aHook.triggerName;
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.addHook(hook);
};
createHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => {
if (result) {
throw new Parse.Error(143,`function name: ${aHook.functionName} already exits`);
} else {
return this.createOrUpdateHook(aHook);
}
});
} else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) {
throw new Parse.Error(143,`class ${aHook.className} already has trigger ${aHook.triggerName}`);
}
return this.createOrUpdateHook(aHook);
});
}
throw new Parse.Error(143, "invalid hook declaration");
};
updateHook(aHook) {
if (aHook.functionName) {
return this.getFunction(aHook.functionName).then((result) => {
if (result) {
return this.createOrUpdateHook(aHook);
}
throw new Parse.Error(143,`no function named: ${aHook.functionName} is defined`);
});
} else if (aHook.className && aHook.triggerName) {
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
if (result) {
return this.createOrUpdateHook(aHook);
}
throw new Parse.Error(143,`class ${aHook.className} does not exist`);
});
}
throw new Parse.Error(143, "invalid hook declaration");
};
load() {
return this.getHooks().then((hooks) => {
hooks = hooks || [];
hooks.forEach((hook) => {
this.addHookToTriggers(hook);
});
});
}
}
function wrapToHTTPRequest(hook) {
return function(request, response) {
var jsonBody = {};
for(var i in request) {
jsonBody[i] = request[i];
}
if (request.object) {
jsonBody.object = request.object.toJSON();
jsonBody.object.className = request.object.className;
}
if (request.original) {
jsonBody.original = request.original.toJSON();
jsonBody.original.className = request.original.className;
}
var jsonRequest = {};
jsonRequest.headers = {
'Content-Type': 'application/json'
}
jsonRequest.body = JSON.stringify(jsonBody);
require("request").post(hook.url, jsonRequest, function(err, res, body){
var result;
if (body) {
if (typeof body == "string") {
try {
body = JSON.parse(body);
} catch(e) {
err = {error: "Malformed response", code: -1};
}
}
if (!err) {
result = body.success;
err = body.error;
}
}
if (err) {
return response.error(err);
} else {
return response.success(result);
}
});
}
}
export default HooksController;

View File

@@ -31,7 +31,7 @@ export default class PromiseRouter {
} }
}; };
route(method, path, handler) { route(method, path, ...handlers) {
switch(method) { switch(method) {
case 'POST': case 'POST':
case 'GET': case 'GET':
@@ -42,6 +42,25 @@ export default class PromiseRouter {
throw 'cannot route method: ' + method; throw 'cannot route method: ' + method;
} }
let handler = handlers[0];
if (handlers.length > 1) {
const length = handlers.length;
handler = function(req) {
var next = function(i, req, res) {
if (i == length) {
return res;
}
let result = handlers[i](req);
if (!result || typeof result.then !== "function") {
result = Promise.resolve(result);
}
return result.then((res) => (next(i+1, req, res)));
}
return next(0, req);
}
}
this.routes.push({ this.routes.push({
path: path, path: path,
method: method, method: method,
@@ -58,7 +77,6 @@ export default class PromiseRouter {
if (route.method != method) { if (route.method != method) {
continue; continue;
} }
// NOTE: we can only route the specific wildcards :className and // NOTE: we can only route the specific wildcards :className and
// :objectId, and in that order. // :objectId, and in that order.
// This is pretty hacky but I don't want to rebuild the entire // This is pretty hacky but I don't want to rebuild the entire

View File

@@ -134,7 +134,7 @@ RestWrite.prototype.runBeforeTrigger = function() {
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
return triggers.maybeRunTrigger( return triggers.maybeRunTrigger(
'beforeSave', this.auth, updatedObject, originalObject); 'beforeSave', this.auth, updatedObject, originalObject, this.config.applicationId);
}).then((response) => { }).then((response) => {
if (response && response.object) { if (response && response.object) {
this.data = response.object; this.data = response.object;
@@ -789,7 +789,7 @@ RestWrite.prototype.runAfterTrigger = function() {
originalObject = triggers.inflate(extraData, this.originalData); originalObject = triggers.inflate(extraData, this.originalData);
} }
triggers.maybeRunTrigger('afterSave', this.auth, inflatedObject, originalObject); triggers.maybeRunTrigger('afterSave', this.auth, inflatedObject, originalObject, this.config.applicationId);
}; };
// A helper to figure out what location this operation happens at. // A helper to figure out what location this operation happens at.

View File

@@ -1,4 +1,3 @@
import PromiseRouter from '../PromiseRouter';
import express from 'express'; import express from 'express';
import BodyParser from 'body-parser'; import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares'; import * as Middlewares from '../middlewares';

View File

@@ -1,7 +1,8 @@
// functions.js // FunctionsRouter.js
var express = require('express'), var express = require('express'),
Parse = require('parse/node').Parse; Parse = require('parse/node').Parse,
triggers = require('../triggers');
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
@@ -27,17 +28,21 @@ export class FunctionsRouter extends PromiseRouter {
} }
static handleCloudFunction(req) { static handleCloudFunction(req) {
if (Parse.Cloud.Functions[req.params.functionName]) { var applicationId = req.config.applicationId;
var theFunction = triggers.getFunction(req.params.functionName, applicationId);
var theValidator = triggers.getValidator(req.params.functionName, applicationId);
if (theFunction) {
const params = Object.assign({}, req.body, req.query);
var request = { var request = {
params: Object.assign({}, req.body, req.query), params: params,
master: req.auth && req.auth.isMaster, master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user, user: req.auth && req.auth.user,
installationId: req.info.installationId installationId: req.info.installationId
}; };
if (Parse.Cloud.Validators[req.params.functionName]) { if (theValidator && typeof theValidator === "function") {
var result = Parse.Cloud.Validators[req.params.functionName](request); var result = theValidator(request);
if (!result) { if (!result) {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.'); throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Validation failed.');
} }
@@ -45,7 +50,11 @@ export class FunctionsRouter extends PromiseRouter {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var response = FunctionsRouter.createResponseObject(resolve, reject); var response = FunctionsRouter.createResponseObject(resolve, reject);
Parse.Cloud.Functions[req.params.functionName](request, response); // Force the keys before the function calls.
Parse.applicationId = req.config.applicationId;
Parse.javascriptKey = req.config.javascriptKey;
Parse.masterKey = req.config.masterKey;
theFunction(request, response);
}); });
} else { } else {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.'); throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.');

107
src/Routers/HooksRouter.js Normal file
View File

@@ -0,0 +1,107 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import { HooksController } from '../Controllers/HooksController';
function enforceMasterKeyAccess(req) {
if (!req.auth.isMaster) {
throw new Parse.Error(403, "unauthorized: master key is required");
}
}
export class HooksRouter extends PromiseRouter {
createHook(aHook, config) {
return config.hooksController.createHook(aHook).then( (hook) => ({response: hook}));
};
updateHook(aHook, config) {
return config.hooksController.updateHook(aHook).then((hook) => ({response: hook}));
};
handlePost(req) {
return this.createHook(req.body, req.config);
};
handleGetFunctions(req) {
var hooksController = req.config.hooksController;
if (req.params.functionName) {
return hooksController.getFunction(req.params.functionName).then( (foundFunction) => {
if (!foundFunction) {
throw new Parse.Error(143, `no function named: ${req.params.functionName} is defined`);
}
return Promise.resolve({response: foundFunction});
});
}
return hooksController.getFunctions().then((functions) => {
return { response: functions || [] };
}, (err) => {
throw err;
});
}
handleGetTriggers(req) {
var hooksController = req.config.hooksController;
if (req.params.className && req.params.triggerName) {
return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => {
if (!foundTrigger) {
throw new Parse.Error(143,`class ${req.params.className} does not exist`);
}
return Promise.resolve({response: foundTrigger});
});
}
return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] }));
}
handleDelete(req) {
var hooksController = req.config.hooksController;
if (req.params.functionName) {
return hooksController.deleteFunction(req.params.functionName).then(() => ({response: {}}))
} else if (req.params.className && req.params.triggerName) {
return hooksController.deleteTrigger(req.params.className, req.params.triggerName).then(() => ({response: {}}))
}
return Promise.resolve({response: {}});
}
handleUpdate(req) {
var hook;
if (req.params.functionName && req.body.url) {
hook = {}
hook.functionName = req.params.functionName;
hook.url = req.body.url;
} else if (req.params.className && req.params.triggerName && req.body.url) {
hook = {}
hook.className = req.params.className;
hook.triggerName = req.params.triggerName;
hook.url = req.body.url
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.updateHook(hook, req.config);
}
handlePut(req) {
var body = req.body;
if (body.__op == "Delete") {
return this.handleDelete(req);
} else {
return this.handleUpdate(req);
}
}
mountRoutes() {
this.route('GET', '/hooks/functions', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
this.route('GET', '/hooks/triggers', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
this.route('GET', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
this.route('GET', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
this.route('POST', '/hooks/functions', enforceMasterKeyAccess, this.handlePost.bind(this));
this.route('POST', '/hooks/triggers', enforceMasterKeyAccess, this.handlePost.bind(this));
this.route('PUT', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handlePut.bind(this));
this.route('PUT', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, this.handlePut.bind(this));
}
}
export default HooksRouter;

View File

@@ -0,0 +1,51 @@
var Parse = require("parse/node");
var triggers = require("../triggers");
function validateClassNameForTriggers(className) {
const restrictedClassNames = [ '_Session' ];
if (restrictedClassNames.indexOf(className) != -1) {
throw `Triggers are not supported for ${className} class.`;
}
return className;
}
function getClassName(parseClass) {
if (parseClass && parseClass.className) {
return validateClassNameForTriggers(parseClass.className);
}
return validateClassNameForTriggers(parseClass);
}
var ParseCloud = {};
ParseCloud.define = function(functionName, handler, validationHandler) {
triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId);
};
ParseCloud.beforeSave = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger('beforeSave', className, handler, Parse.applicationId);
};
ParseCloud.beforeDelete = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger('beforeDelete', className, handler, Parse.applicationId);
};
ParseCloud.afterSave = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger('afterSave', className, handler, Parse.applicationId);
};
ParseCloud.afterDelete = function(parseClass, handler) {
var className = getClassName(parseClass);
triggers.addTrigger('afterDelete', className, handler, Parse.applicationId);
};
ParseCloud._removeHook = function(category, name, type, applicationId) {
applicationId = applicationId || Parse.applicationId;
triggers._unregister(applicationId, category, name, type);
};
ParseCloud.httpRequest = require("./httpRequest");
module.exports = ParseCloud;

View File

@@ -0,0 +1,132 @@
var request = require("request");
const send = function(method, path, body) {
var Parse = require("parse/node").Parse;
var options = {
method: method,
url: Parse.serverURL + path,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'application/json'
},
};
if (body) {
if (typeof body == "object") {
options.body = JSON.stringify(body);
} else {
options.body = body;
}
}
var promise = new Parse.Promise();
request(options, function(err, response, body){
if (err) {
promise.reject(err);
return;
}
body = JSON.parse(body);
if (body.error) {
promise.reject(body);
} else {
promise.resolve(body);
}
});
return promise;
}
var Hooks = {};
Hooks.getFunctions = function() {
return Hooks.get("functions");
}
Hooks.getTriggers = function() {
return Hooks.get("triggers");
}
Hooks.getFunction = function(name) {
return Hooks.get("functions", name);
}
Hooks.getTrigger = function(className, triggerName) {
return Hooks.get("triggers", className, triggerName);
}
Hooks.get = function(type, functionName, triggerName) {
var url = "/hooks/"+type;
if(functionName) {
url += "/"+functionName;
if (triggerName) {
url += "/"+triggerName;
}
}
return send("GET", url);
}
Hooks.createFunction = function(functionName, url) {
return Hooks.create({functionName: functionName, url: url});
}
Hooks.createTrigger = function(className, triggerName, url) {
return Hooks.create({className: className, triggerName: triggerName, url: url});
}
Hooks.create = function(hook) {
var url;
if (hook.functionName && hook.url) {
url = "/hooks/functions";
} else if (hook.className && hook.triggerName && hook.url) {
url = "/hooks/triggers";
} else {
return Promise.reject({error: 'invalid hook declaration', code: 143});
}
return send("POST", url, hook);
}
Hooks.updateFunction = function(functionName, url) {
return Hooks.update({functionName: functionName, url: url});
}
Hooks.updateTrigger = function(className, triggerName, url) {
return Hooks.update({className: className, triggerName: triggerName, url: url});
}
Hooks.update = function(hook) {
var url;
if (hook.functionName && hook.url) {
url = "/hooks/functions/"+hook.functionName;
delete hook.functionName;
} else if (hook.className && hook.triggerName && hook.url) {
url = "/hooks/triggers/"+hook.className+"/"+hook.triggerName;
delete hook.className;
delete hook.triggerName;
}
return send("PUT", url, hook);
}
Hooks.deleteFunction = function(functionName) {
return Hooks.delete({functionName: functionName});
}
Hooks.deleteTrigger = function(className, triggerName) {
return Hooks.delete({className: className, triggerName: triggerName});
}
Hooks.delete = function(hook) {
var url;
if (hook.functionName) {
url = "/hooks/functions/"+hook.functionName;
delete hook.functionName;
} else if (hook.className && hook.triggerName) {
url = "/hooks/triggers/"+hook.className+"/"+hook.triggerName;
delete hook.className;
delete hook.triggerName;
}
return send("PUT", url, '{ "__op": "Delete" }');
}
module.exports = Hooks

View File

@@ -9,8 +9,7 @@ var batch = require('./batch'),
express = require('express'), express = require('express'),
middlewares = require('./middlewares'), middlewares = require('./middlewares'),
multer = require('multer'), multer = require('multer'),
Parse = require('parse/node').Parse, Parse = require('parse/node').Parse;
httpRequest = require('./httpRequest');
import PromiseRouter from './PromiseRouter'; import PromiseRouter from './PromiseRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
@@ -32,10 +31,12 @@ import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { PushRouter } from './Routers/PushRouter'; import { PushRouter } from './Routers/PushRouter';
import { FilesRouter } from './Routers/FilesRouter'; import { FilesRouter } from './Routers/FilesRouter';
import { LogsRouter } from './Routers/LogsRouter'; import { LogsRouter } from './Routers/LogsRouter';
import { HooksRouter } from './Routers/HooksRouter';
import { loadAdapter } from './Adapters/AdapterLoader'; import { loadAdapter } from './Adapters/AdapterLoader';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController'; import { LoggerController } from './Controllers/LoggerController';
import { HooksController } from './Controllers/HooksController';
import requiredParameter from './requiredParameter'; import requiredParameter from './requiredParameter';
// Mutate the Parse object to add the Cloud Code handlers // Mutate the Parse object to add the Cloud Code handlers
@@ -88,6 +89,11 @@ function ParseServer({
serverURL = requiredParameter('You must provide a serverURL!'), serverURL = requiredParameter('You must provide a serverURL!'),
maxUploadSize = '20mb' maxUploadSize = '20mb'
}) { }) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || '', masterKey);
Parse.serverURL = serverURL || '';
if (databaseAdapter) { if (databaseAdapter) {
DatabaseAdapter.setAdapter(databaseAdapter); DatabaseAdapter.setAdapter(databaseAdapter);
} }
@@ -95,6 +101,7 @@ function ParseServer({
if (databaseURI) { if (databaseURI) {
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
} }
if (cloud) { if (cloud) {
addParseCloud(); addParseCloud();
if (typeof cloud === 'function') { if (typeof cloud === 'function') {
@@ -106,7 +113,6 @@ function ParseServer({
} }
} }
const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter); const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter);
const pushControllerAdapter = loadAdapter(push, ParsePushAdapter); const pushControllerAdapter = loadAdapter(push, ParsePushAdapter);
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
@@ -116,6 +122,7 @@ function ParseServer({
const filesController = new FilesController(filesControllerAdapter); const filesController = new FilesController(filesControllerAdapter);
const pushController = new PushController(pushControllerAdapter); const pushController = new PushController(pushControllerAdapter);
const loggerController = new LoggerController(loggerControllerAdapter); const loggerController = new LoggerController(loggerControllerAdapter);
const hooksController = new HooksController(appId);
cache.apps[appId] = { cache.apps[appId] = {
masterKey: masterKey, masterKey: masterKey,
@@ -129,6 +136,7 @@ function ParseServer({
filesController: filesController, filesController: filesController,
pushController: pushController, pushController: pushController,
loggerController: loggerController, loggerController: loggerController,
hooksController: hooksController,
enableAnonymousUsers: enableAnonymousUsers, enableAnonymousUsers: enableAnonymousUsers,
oauth: oauth, oauth: oauth,
}; };
@@ -138,10 +146,6 @@ function ParseServer({
cache.apps[appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); cache.apps[appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
} }
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey, masterKey);
Parse.serverURL = serverURL;
// This app serves the Parse API directly. // This app serves the Parse API directly.
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API. // It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
var api = express(); var api = express();
@@ -179,6 +183,10 @@ function ParseServer({
routers.push(require('./global_config')); routers.push(require('./global_config'));
} }
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
routers.push(new HooksRouter());
}
let appRouter = new PromiseRouter(); let appRouter = new PromiseRouter();
routers.forEach((router) => { routers.forEach((router) => {
appRouter.merge(router); appRouter.merge(router);
@@ -189,7 +197,6 @@ function ParseServer({
api.use(middlewares.handleParseErrors); api.use(middlewares.handleParseErrors);
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error
console.log(`Unable to listen on port ${err.port}. The port is already in use.`); console.log(`Unable to listen on port ${err.port}. The port is already in use.`);
@@ -199,52 +206,14 @@ function ParseServer({
throw err; throw err;
} }
}); });
hooksController.load();
return api; return api;
} }
function addParseCloud() { function addParseCloud() {
Parse.Cloud.Functions = {}; const ParseCloud = require("./cloud-code/Parse.Cloud");
Parse.Cloud.Validators = {}; Object.assign(Parse.Cloud, ParseCloud);
Parse.Cloud.Triggers = {
beforeSave: {},
beforeDelete: {},
afterSave: {},
afterDelete: {}
};
function validateClassNameForTriggers(className) {
const restrictedClassNames = [ '_Session' ];
if (restrictedClassNames.indexOf(className) != -1) {
throw `Triggers are not supported for ${className} class.`;
}
}
Parse.Cloud.define = function(functionName, handler, validationHandler) {
Parse.Cloud.Functions[functionName] = handler;
Parse.Cloud.Validators[functionName] = validationHandler;
};
Parse.Cloud.beforeSave = function(parseClass, handler) {
let className = getClassName(parseClass);
validateClassNameForTriggers(className);
Parse.Cloud.Triggers.beforeSave[className] = handler;
};
Parse.Cloud.beforeDelete = function(parseClass, handler) {
let className = getClassName(parseClass);
validateClassNameForTriggers(className);
Parse.Cloud.Triggers.beforeDelete[className] = handler;
};
Parse.Cloud.afterSave = function(parseClass, handler) {
let className = getClassName(parseClass);
validateClassNameForTriggers(className);
Parse.Cloud.Triggers.afterSave[className] = handler;
};
Parse.Cloud.afterDelete = function(parseClass, handler) {
let className = getClassName(parseClass);
validateClassNameForTriggers(className);
Parse.Cloud.Triggers.afterDelete[className] = handler;
};
Parse.Cloud.httpRequest = httpRequest;
global.Parse = Parse; global.Parse = Parse;
} }

View File

@@ -39,8 +39,8 @@ function del(config, auth, className, objectId) {
var inflatedObject; var inflatedObject;
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
if (triggers.getTrigger(className, 'beforeDelete') || if (triggers.getTrigger(className, 'beforeDelete', config.applicationId) ||
triggers.getTrigger(className, 'afterDelete') || triggers.getTrigger(className, 'afterDelete', config.applicationId) ||
className == '_Session') { className == '_Session') {
return find(config, auth, className, {objectId: objectId}) return find(config, auth, className, {objectId: objectId})
.then((response) => { .then((response) => {
@@ -49,7 +49,7 @@ function del(config, auth, className, objectId) {
cache.clearUser(response.results[0].sessionToken); cache.clearUser(response.results[0].sessionToken);
inflatedObject = Parse.Object.fromJSON(response.results[0]); inflatedObject = Parse.Object.fromJSON(response.results[0]);
return triggers.maybeRunTrigger('beforeDelete', return triggers.maybeRunTrigger('beforeDelete',
auth, inflatedObject); auth, inflatedObject, null, config.applicationId);
} }
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found for delete.'); 'Object not found for delete.');
@@ -76,7 +76,7 @@ function del(config, auth, className, objectId) {
objectId: objectId objectId: objectId
}, options); }, options);
}).then(() => { }).then(() => {
triggers.maybeRunTrigger('afterDelete', auth, inflatedObject); triggers.maybeRunTrigger('afterDelete', auth, inflatedObject, null, config.applicationId);
return Promise.resolve(); return Promise.resolve();
}); });
} }
@@ -96,8 +96,8 @@ function update(config, auth, className, objectId, restObject) {
enforceRoleSecurity('update', className, auth); enforceRoleSecurity('update', className, auth);
return Promise.resolve().then(() => { return Promise.resolve().then(() => {
if (triggers.getTrigger(className, 'beforeSave') || if (triggers.getTrigger(className, 'beforeSave', config.applicationId) ||
triggers.getTrigger(className, 'afterSave')) { triggers.getTrigger(className, 'afterSave', config.applicationId)) {
return find(config, auth, className, {objectId: objectId}); return find(config, auth, className, {objectId: objectId});
} }
return Promise.resolve({}); return Promise.resolve({});

View File

@@ -1,6 +1,6 @@
// triggers.js // triggers.js
var Parse = require('parse/node').Parse,
var Parse = require('parse/node').Parse; cache = require('./cache');
var Types = { var Types = {
beforeSave: 'beforeSave', beforeSave: 'beforeSave',
@@ -9,11 +9,60 @@ var Types = {
afterDelete: 'afterDelete' afterDelete: 'afterDelete'
}; };
var getTrigger = function(className, triggerType) { var BaseStore = function() {
if (Parse.Cloud.Triggers this.Functions = {}
&& Parse.Cloud.Triggers[triggerType] this.Validators = {}
&& Parse.Cloud.Triggers[triggerType][className]) { this.Triggers = Object.keys(Types).reduce(function(base, key){
return Parse.Cloud.Triggers[triggerType][className]; base[key] = {};
return base;
}, {});
}
var _triggerStore = {};
function addFunction(functionName, handler, validationHandler, applicationId) {
applicationId = applicationId || Parse.applicationId;
_triggerStore[applicationId] = _triggerStore[applicationId] || new BaseStore();
_triggerStore[applicationId].Functions[functionName] = handler;
_triggerStore[applicationId].Validators[functionName] = validationHandler;
}
function addTrigger(type, className, handler, applicationId) {
applicationId = applicationId || Parse.applicationId;
_triggerStore[applicationId] = _triggerStore[applicationId] || new BaseStore();
_triggerStore[applicationId].Triggers[type][className] = handler;
}
function removeFunction(functionName, applicationId) {
applicationId = applicationId || Parse.applicationId;
delete _triggerStore[applicationId].Functions[functionName]
}
function removeTrigger(type, className, applicationId) {
applicationId = applicationId || Parse.applicationId;
delete _triggerStore[applicationId].Triggers[type][className]
}
function _unregister(a,b,c,d) {
if (d) {
removeTrigger(c,d,a);
delete _triggerStore[a][b][c][d];
} else {
delete _triggerStore[a][b][c];
}
}
var getTrigger = function(className, triggerType, applicationId) {
if (!applicationId) {
throw "Missing ApplicationID";
}
var manager = _triggerStore[applicationId]
if (manager
&& manager.Triggers
&& manager.Triggers[triggerType]
&& manager.Triggers[triggerType][className]) {
return manager.Triggers[triggerType][className];
} }
return undefined; return undefined;
}; };
@@ -22,6 +71,22 @@ function triggerExists(className: string, type: string): boolean {
return (getTrigger(className, type) != undefined); return (getTrigger(className, type) != undefined);
} }
var getFunction = function(functionName, applicationId) {
var manager = _triggerStore[applicationId];
if (manager && manager.Functions) {
return manager.Functions[functionName];
};
return undefined;
}
var getValidator = function(functionName, applicationId) {
var manager = _triggerStore[applicationId];
if (manager && manager.Validators) {
return manager.Validators[functionName];
};
return undefined;
}
var getRequestObject = function(triggerType, auth, parseObject, originalParseObject) { var getRequestObject = function(triggerType, auth, parseObject, originalParseObject) {
var request = { var request = {
triggerName: triggerType, triggerName: triggerType,
@@ -53,8 +118,12 @@ var getRequestObject = function(triggerType, auth, parseObject, originalParseObj
// Any changes made to the object in a beforeSave will be included. // Any changes made to the object in a beforeSave will be included.
var getResponseObject = function(request, resolve, reject) { var getResponseObject = function(request, resolve, reject) {
return { return {
success: function() { success: function(response) {
var response = {}; // Use the JSON response
if (response && request.triggerName === Types.beforeSave) {
return resolve(response);
}
response = {};
if (request.triggerName === Types.beforeSave) { if (request.triggerName === Types.beforeSave) {
response['object'] = request.object._getSaveJSON(); response['object'] = request.object._getSaveJSON();
} }
@@ -72,15 +141,19 @@ var getResponseObject = function(request, resolve, reject) {
// Resolves to an object, empty or containing an object key. A beforeSave // Resolves to an object, empty or containing an object key. A beforeSave
// trigger will set the object key to the rest format object to save. // trigger will set the object key to the rest format object to save.
// originalParseObject is optional, we only need that for befote/afterSave functions // originalParseObject is optional, we only need that for befote/afterSave functions
var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObject) { var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObject, applicationId) {
if (!parseObject) { if (!parseObject) {
return Promise.resolve({}); return Promise.resolve({});
} }
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var trigger = getTrigger(parseObject.className, triggerType); var trigger = getTrigger(parseObject.className, triggerType, applicationId);
if (!trigger) return resolve({}); if (!trigger) return resolve();
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject); var request = getRequestObject(triggerType, auth, parseObject, originalParseObject);
var response = getResponseObject(request, resolve, reject); var response = getResponseObject(request, resolve, reject);
// Force the current Parse app before the trigger
Parse.applicationId = applicationId;
Parse.javascriptKey = cache.apps[applicationId].javascriptKey || '';
Parse.masterKey = cache.apps[applicationId].masterKey;
trigger(request, response); trigger(request, response);
}); });
}; };
@@ -95,6 +168,7 @@ function inflate(data, restObject) {
return Parse.Object.fromJSON(copy); return Parse.Object.fromJSON(copy);
} }
<<<<<<< 5fae41183ed476976ff29a4c247aa78b00b83a9e
module.exports = { module.exports = {
getTrigger: getTrigger, getTrigger: getTrigger,
getRequestObject: getRequestObject, getRequestObject: getRequestObject,
@@ -103,3 +177,21 @@ module.exports = {
triggerExists: triggerExists, triggerExists: triggerExists,
Types: Types Types: Types
}; };
=======
var TriggerManager = {};
TriggerManager.getTrigger = getTrigger;
TriggerManager.getRequestObject = getRequestObject;
TriggerManager.inflate = inflate;
TriggerManager.maybeRunTrigger = maybeRunTrigger;
TriggerManager.Types = Types;
TriggerManager.addFunction = addFunction;
TriggerManager.getFunction = getFunction;
TriggerManager.removeTrigger = removeTrigger;
TriggerManager.removeFunction = removeFunction;
TriggerManager.getValidator = getValidator;
TriggerManager.addTrigger = addTrigger;
TriggerManager._unregister = _unregister;
module.exports = TriggerManager;
>>>>>>> Adds Hooks API