From 9ac7a52e403e8259e02104908e2388a07f119447 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 5 Feb 2016 14:38:09 -0500 Subject: [PATCH 1/4] Adds Hooks API Adds Parse.Hooks.js in src/cloud-code/Parse.Hooks.js Moves Cloud code related functions in src/cloud-code --- .gitignore | 6 + spec/HTTPRequest.spec.js | 2 +- spec/ParseACL.spec.js | 6 +- spec/ParseAPI.spec.js | 44 ++-- spec/ParseHooks.spec.js | 390 ++++++++++++++++++++++++++++ spec/cloud/main.js | 2 - src/Config.js | 3 +- src/Controllers/HooksController.js | 229 ++++++++++++++++ src/PromiseRouter.js | 22 +- src/RestWrite.js | 6 +- src/Routers/FilesRouter.js | 1 - src/Routers/FunctionsRouter.js | 23 +- src/Routers/HooksRouter.js | 107 ++++++++ src/cloud-code/Parse.Cloud.js | 51 ++++ src/cloud-code/Parse.Hooks.js | 132 ++++++++++ src/{ => cloud-code}/httpRequest.js | 0 src/index.js | 73 ++---- src/rest.js | 12 +- src/triggers.js | 116 ++++++++- 19 files changed, 1121 insertions(+), 104 deletions(-) create mode 100644 spec/ParseHooks.spec.js create mode 100644 src/Controllers/HooksController.js create mode 100644 src/Routers/HooksRouter.js create mode 100644 src/cloud-code/Parse.Cloud.js create mode 100644 src/cloud-code/Parse.Hooks.js rename src/{ => cloud-code}/httpRequest.js (100%) diff --git a/.gitignore b/.gitignore index 318fed2034..de88257b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,11 @@ node_modules # WebStorm/IntelliJ .idea +# visual studio code +.vscode + # Babel.js lib/ + +# cache folder +.cache diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index 01efe42b08..e599dd5d89 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -1,4 +1,4 @@ -var httpRequest = require("../src/httpRequest"), +var httpRequest = require("../src/cloud-code/httpRequest"), bodyParser = require('body-parser'), express = require("express"); diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index f7cabf2ac2..62b30b0660 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -786,7 +786,11 @@ describe('Parse.ACL', () => { equal(results.length, 1); var result = results[0]; ok(result); - equal(result.id, object.id); + if (!result) { + fail("should have result"); + } else { + equal(result.id, object.id); + } done(); } }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 0eb0314a99..0901b4dfa4 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -275,7 +275,11 @@ describe('miscellaneous', function() { var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); return objAgain.fetch(); }).then((objAgain) => { - expect(objAgain.get('foo')).toEqual('bar'); + if (objAgain) { + expect(objAgain.get('foo')).toEqual('bar'); + } else { + fail("unable to fetch the object ", id); + } done(); }, (error) => { // 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) { Parse.Cloud.run('foo').then((result) => { 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.get('x')).toEqual(2); var bar = result.object.get('relation'); @@ -381,23 +390,25 @@ describe('miscellaneous', function() { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); done(); - }); + }).fail( err => { + fail(err); + done(); + }) }); describe('beforeSave', () => { beforeEach(done => { // Make sure the required mock for all tests is unset. - delete Parse.Cloud.Triggers.beforeSave.GameScore; + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }); - afterEach(done => { // Make sure the required mock for all tests is unset. - delete Parse.Cloud.Triggers.beforeSave.GameScore; + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); - }); - - it('object is set on create and update', done => { + }); + + it('object is set on create and update', done => { let triggerTime = 0; // Register a mock beforeSave hook Parse.Cloud.beforeSave('GameScore', (req, res) => { @@ -610,8 +621,8 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - delete Parse.Cloud.Triggers.afterSave.GameScore; + // Clear mock beforeSave + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, function(error) { fail(error); @@ -663,9 +674,10 @@ describe('miscellaneous', function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); // Clear mock afterSave - delete Parse.Cloud.Triggers.afterSave.GameScore; + Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { + console.error(error); fail(error); done(); }); @@ -678,12 +690,12 @@ describe('miscellaneous', function() { }); Parse.Cloud.run('willFail').then((s) => { fail('Should not have succeeded.'); - delete Parse.Cloud.Functions['willFail']; + Parse.Cloud._removeHook("Functions", "willFail"); done(); }, (e) => { expect(e.code).toEqual(141); expect(e.message).toEqual('noway'); - delete Parse.Cloud.Functions['willFail']; + Parse.Cloud._removeHook("Functions", "willFail"); done(); }); }); @@ -712,7 +724,7 @@ describe('miscellaneous', function() { // Make sure query string params override body params expect(res.other).toEqual('2'); expect(res.foo).toEqual("bar"); - delete Parse.Cloud.Functions['echoParams']; + Parse.Cloud._removeHook("Functions",'echoParams'); done(); }); }); @@ -726,7 +738,7 @@ describe('miscellaneous', function() { }); Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { - delete Parse.Cloud.Functions['functionWithParameterValidation']; + Parse.Cloud._removeHook("Functions", "functionWithParameterValidation"); done(); }, (e) => { fail('Validation should not have failed.'); @@ -744,7 +756,7 @@ describe('miscellaneous', function() { Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => { fail('Validation should not have succeeded'); - delete Parse.Cloud.Functions['functionWithParameterValidationFailure']; + Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure"); done(); }, (e) => { expect(e.code).toEqual(141); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js new file mode 100644 index 0000000000..e24211383e --- /dev/null +++ b/spec/ParseHooks.spec.js @@ -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(); + }); + }); +}); \ No newline at end of file diff --git a/spec/cloud/main.js b/spec/cloud/main.js index fec259910a..9e53e6376a 100644 --- a/spec/cloud/main.js +++ b/spec/cloud/main.js @@ -1,5 +1,3 @@ -var Parse = require('parse/node').Parse; - Parse.Cloud.define('hello', function(req, res) { res.success('Hello world!'); }); diff --git a/src/Config.js b/src/Config.js index 4859c09c85..b2e7428caa 100644 --- a/src/Config.js +++ b/src/Config.js @@ -1,6 +1,7 @@ // A Config object provides information about how a specific app is // configured. // mount is the URL for the root of the API; includes http, domain, etc. + export class Config { constructor(applicationId, mount) { @@ -23,8 +24,8 @@ export class Config { this.fileKey = cacheInfo.fileKey; this.facebookAppIds = cacheInfo.facebookAppIds; this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; - this.database = DatabaseAdapter.getDatabaseConnection(applicationId); + this.hooksController = cacheInfo.hooksController; this.filesController = cacheInfo.filesController; this.pushController = cacheInfo.pushController; this.loggerController = cacheInfo.loggerController; diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js new file mode 100644 index 0000000000..9de061bfbf --- /dev/null +++ b/src/Controllers/HooksController.js @@ -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; diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index 498e7d4920..8155c7967b 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -31,7 +31,7 @@ export default class PromiseRouter { } }; - route(method, path, handler) { + route(method, path, ...handlers) { switch(method) { case 'POST': case 'GET': @@ -42,6 +42,25 @@ export default class PromiseRouter { 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({ path: path, method: method, @@ -58,7 +77,6 @@ export default class PromiseRouter { if (route.method != method) { continue; } - // NOTE: we can only route the specific wildcards :className and // :objectId, and in that order. // This is pretty hacky but I don't want to rebuild the entire diff --git a/src/RestWrite.js b/src/RestWrite.js index 2f6d88bab5..1630f86b49 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -134,7 +134,7 @@ RestWrite.prototype.runBeforeTrigger = function() { return Promise.resolve().then(() => { return triggers.maybeRunTrigger( - 'beforeSave', this.auth, updatedObject, originalObject); + 'beforeSave', this.auth, updatedObject, originalObject, this.config.applicationId); }).then((response) => { if (response && response.object) { this.data = response.object; @@ -294,7 +294,7 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { if (!validateAuthData || !validateAppId) { return false; }; - + return validateAuthData(authData, oauthOptions) .then(() => { if (appIds && typeof validateAppId === "function") { @@ -789,7 +789,7 @@ RestWrite.prototype.runAfterTrigger = function() { 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. diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 653f85b3fc..65da555fab 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -1,4 +1,3 @@ -import PromiseRouter from '../PromiseRouter'; import express from 'express'; import BodyParser from 'body-parser'; import * as Middlewares from '../middlewares'; diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 76190682bc..0902b871da 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -1,7 +1,8 @@ -// functions.js +// FunctionsRouter.js var express = require('express'), - Parse = require('parse/node').Parse; + Parse = require('parse/node').Parse, + triggers = require('../triggers'); import PromiseRouter from '../PromiseRouter'; @@ -27,17 +28,21 @@ export class FunctionsRouter extends PromiseRouter { } 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 = { - params: Object.assign({}, req.body, req.query), + params: params, master: req.auth && req.auth.isMaster, user: req.auth && req.auth.user, installationId: req.info.installationId }; - if (Parse.Cloud.Validators[req.params.functionName]) { - var result = Parse.Cloud.Validators[req.params.functionName](request); + if (theValidator && typeof theValidator === "function") { + var result = theValidator(request); if (!result) { 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) { 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 { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.'); diff --git a/src/Routers/HooksRouter.js b/src/Routers/HooksRouter.js new file mode 100644 index 0000000000..ed34cdc4c0 --- /dev/null +++ b/src/Routers/HooksRouter.js @@ -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; diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js new file mode 100644 index 0000000000..c859b37e0d --- /dev/null +++ b/src/cloud-code/Parse.Cloud.js @@ -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; diff --git a/src/cloud-code/Parse.Hooks.js b/src/cloud-code/Parse.Hooks.js new file mode 100644 index 0000000000..4bb8d33c37 --- /dev/null +++ b/src/cloud-code/Parse.Hooks.js @@ -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 diff --git a/src/httpRequest.js b/src/cloud-code/httpRequest.js similarity index 100% rename from src/httpRequest.js rename to src/cloud-code/httpRequest.js diff --git a/src/index.js b/src/index.js index 0c0fff6dc2..6b48018e2e 100644 --- a/src/index.js +++ b/src/index.js @@ -9,8 +9,7 @@ var batch = require('./batch'), express = require('express'), middlewares = require('./middlewares'), multer = require('multer'), - Parse = require('parse/node').Parse, - httpRequest = require('./httpRequest'); + Parse = require('parse/node').Parse; import PromiseRouter from './PromiseRouter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; @@ -32,10 +31,12 @@ import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { PushRouter } from './Routers/PushRouter'; import { FilesRouter } from './Routers/FilesRouter'; import { LogsRouter } from './Routers/LogsRouter'; +import { HooksRouter } from './Routers/HooksRouter'; import { loadAdapter } from './Adapters/AdapterLoader'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; +import { HooksController } from './Controllers/HooksController'; import requiredParameter from './requiredParameter'; // Mutate the Parse object to add the Cloud Code handlers @@ -88,6 +89,11 @@ function ParseServer({ serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb' }) { + + // Initialize the node client SDK automatically + Parse.initialize(appId, javascriptKey || '', masterKey); + Parse.serverURL = serverURL || ''; + if (databaseAdapter) { DatabaseAdapter.setAdapter(databaseAdapter); } @@ -95,6 +101,7 @@ function ParseServer({ if (databaseURI) { DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); } + if (cloud) { addParseCloud(); if (typeof cloud === 'function') { @@ -106,7 +113,6 @@ function ParseServer({ } } - const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter); const pushControllerAdapter = loadAdapter(push, ParsePushAdapter); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); @@ -116,7 +122,8 @@ function ParseServer({ const filesController = new FilesController(filesControllerAdapter); const pushController = new PushController(pushControllerAdapter); const loggerController = new LoggerController(loggerControllerAdapter); - + const hooksController = new HooksController(appId); + cache.apps[appId] = { masterKey: masterKey, collectionPrefix: collectionPrefix, @@ -129,19 +136,16 @@ function ParseServer({ filesController: filesController, pushController: pushController, loggerController: loggerController, + hooksController: hooksController, enableAnonymousUsers: enableAnonymousUsers, oauth: oauth, -}; + }; // To maintain compatibility. TODO: Remove in v2.1 if (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. // It's the equivalent of https://api.parse.com/1 in the hosted Parse API. var api = express(); @@ -178,6 +182,10 @@ function ParseServer({ if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { routers.push(require('./global_config')); } + + if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { + routers.push(new HooksRouter()); + } let appRouter = new PromiseRouter(); routers.forEach((router) => { @@ -189,7 +197,6 @@ function ParseServer({ api.use(middlewares.handleParseErrors); - process.on('uncaughtException', (err) => { 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.`); @@ -199,52 +206,14 @@ function ParseServer({ throw err; } }); + hooksController.load(); return api; } function addParseCloud() { - Parse.Cloud.Functions = {}; - Parse.Cloud.Validators = {}; - 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; + const ParseCloud = require("./cloud-code/Parse.Cloud"); + Object.assign(Parse.Cloud, ParseCloud); global.Parse = Parse; } @@ -258,4 +227,4 @@ function getClassName(parseClass) { module.exports = { ParseServer: ParseServer, S3Adapter: S3Adapter -}; +}; \ No newline at end of file diff --git a/src/rest.js b/src/rest.js index 094e8ab63e..35fce41501 100644 --- a/src/rest.js +++ b/src/rest.js @@ -39,8 +39,8 @@ function del(config, auth, className, objectId) { var inflatedObject; return Promise.resolve().then(() => { - if (triggers.getTrigger(className, 'beforeDelete') || - triggers.getTrigger(className, 'afterDelete') || + if (triggers.getTrigger(className, 'beforeDelete', config.applicationId) || + triggers.getTrigger(className, 'afterDelete', config.applicationId) || className == '_Session') { return find(config, auth, className, {objectId: objectId}) .then((response) => { @@ -49,7 +49,7 @@ function del(config, auth, className, objectId) { cache.clearUser(response.results[0].sessionToken); inflatedObject = Parse.Object.fromJSON(response.results[0]); return triggers.maybeRunTrigger('beforeDelete', - auth, inflatedObject); + auth, inflatedObject, null, config.applicationId); } throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.'); @@ -76,7 +76,7 @@ function del(config, auth, className, objectId) { objectId: objectId }, options); }).then(() => { - triggers.maybeRunTrigger('afterDelete', auth, inflatedObject); + triggers.maybeRunTrigger('afterDelete', auth, inflatedObject, null, config.applicationId); return Promise.resolve(); }); } @@ -96,8 +96,8 @@ function update(config, auth, className, objectId, restObject) { enforceRoleSecurity('update', className, auth); return Promise.resolve().then(() => { - if (triggers.getTrigger(className, 'beforeSave') || - triggers.getTrigger(className, 'afterSave')) { + if (triggers.getTrigger(className, 'beforeSave', config.applicationId) || + triggers.getTrigger(className, 'afterSave', config.applicationId)) { return find(config, auth, className, {objectId: objectId}); } return Promise.resolve({}); diff --git a/src/triggers.js b/src/triggers.js index a05f0b659d..486b3f78ed 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,6 @@ // triggers.js - -var Parse = require('parse/node').Parse; +var Parse = require('parse/node').Parse, + cache = require('./cache'); var Types = { beforeSave: 'beforeSave', @@ -9,11 +9,60 @@ var Types = { afterDelete: 'afterDelete' }; -var getTrigger = function(className, triggerType) { - if (Parse.Cloud.Triggers - && Parse.Cloud.Triggers[triggerType] - && Parse.Cloud.Triggers[triggerType][className]) { - return Parse.Cloud.Triggers[triggerType][className]; +var BaseStore = function() { + this.Functions = {} + this.Validators = {} + this.Triggers = Object.keys(Types).reduce(function(base, key){ + 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; }; @@ -22,6 +71,22 @@ function triggerExists(className: string, type: string): boolean { 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 request = { 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. var getResponseObject = function(request, resolve, reject) { return { - success: function() { - var response = {}; + success: function(response) { + // Use the JSON response + if (response && request.triggerName === Types.beforeSave) { + return resolve(response); + } + response = {}; if (request.triggerName === Types.beforeSave) { 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 // trigger will set the object key to the rest format object to save. // 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) { return Promise.resolve({}); } return new Promise(function (resolve, reject) { - var trigger = getTrigger(parseObject.className, triggerType); - if (!trigger) return resolve({}); + var trigger = getTrigger(parseObject.className, triggerType, applicationId); + if (!trigger) return resolve(); var request = getRequestObject(triggerType, auth, parseObject, originalParseObject); 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); }); }; @@ -95,6 +168,7 @@ function inflate(data, restObject) { return Parse.Object.fromJSON(copy); } +<<<<<<< 5fae41183ed476976ff29a4c247aa78b00b83a9e module.exports = { getTrigger: getTrigger, getRequestObject: getRequestObject, @@ -103,3 +177,21 @@ module.exports = { triggerExists: triggerExists, 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 From c0714b9b9dd747286300a0e85ee8e138ce863ddc Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 24 Feb 2016 13:54:24 -0500 Subject: [PATCH 2/4] Shows --port option in README --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index d44f4b97a8..640b637eab 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,20 @@ The standalone Parse Server can be configured using [environment variables](#con Please refer to the [configuration section](#configuration) or help; +To get more help for running the parse-server standalone, you can run: + +`$ npm start -- --help` + +The standalone API server supports loading a configuration file in JSON format: + +`$ npm start -- path/to/your/config.json` + +The default port is 1337, to use a different port set the `--port` option: + +`$ npm start -- --port=8080 path/to/your/config.json` + +Please refer to the [configuration section](#configuration) or help; + You can also install Parse Server globally: `$ npm install -g parse-server` From c4abd1e2676b8a45a81cbd0ca7abaf144fdeca40 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 24 Feb 2016 15:55:11 -0500 Subject: [PATCH 3/4] cache as ES6 --- spec/helper.js | 2 +- src/Auth.js | 2 +- src/Config.js | 4 +- src/DatabaseAdapter.js | 2 +- src/RestWrite.js | 4 +- src/cache.js | 40 ++++++++++---------- src/index.js | 2 +- src/middlewares.js | 3 +- src/rest.js | 2 +- src/testing-routes.js | 2 +- src/triggers.js | 83 ++++++++++++++++-------------------------- 11 files changed, 64 insertions(+), 82 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 9ad56d3d16..c7474afe96 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -2,7 +2,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; -var cache = require('../src/cache'); +var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); var express = require('express'); var facebook = require('../src/oauth/facebook'); diff --git a/src/Auth.js b/src/Auth.js index 27bbf885b0..642f34ab84 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -2,7 +2,7 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); -var cache = require('./cache'); +import cache from './cache'; // An Auth object tells you who is requesting something and whether // the master key was used. diff --git a/src/Config.js b/src/Config.js index b2e7428caa..510ebf001b 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,10 +2,12 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. +import cache from './cache'; + export class Config { constructor(applicationId, mount) { - var cache = require('./cache'); + var DatabaseAdapter = require('./DatabaseAdapter'); var cacheInfo = cache.apps[applicationId]; diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index b044c46531..904e6c3e7d 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -13,11 +13,11 @@ // * This list is incomplete and the database process is not fully modularized. // // Default is ExportAdapter, which uses mongo. +import cache from './cache'; var ExportAdapter = require('./ExportAdapter'); var adapter = ExportAdapter; -var cache = require('./cache'); var dbConnections = {}; var databaseURI = 'mongodb://localhost:27017/parse'; var appDatabaseURIs = {}; diff --git a/src/RestWrite.js b/src/RestWrite.js index 1630f86b49..d02f93fa84 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -2,10 +2,10 @@ // that writes to the database. // This could be either a "create" or an "update". +import cache from './cache'; var deepcopy = require('deepcopy'); var Auth = require('./Auth'); -var cache = require('./cache'); var Config = require('./Config'); var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); @@ -114,7 +114,7 @@ RestWrite.prototype.validateSchema = function() { // Any change leads to our data being mutated. RestWrite.prototype.runBeforeTrigger = function() { // Avoid doing any setup for triggers if there is no 'beforeSave' trigger for this class. - if (!triggers.triggerExists(this.className, triggers.Types.beforeSave)) { + if (!triggers.triggerExists(this.className, triggers.Types.beforeSave, this.config.applicationId)) { return Promise.resolve(); } diff --git a/src/cache.js b/src/cache.js index eeb2803178..a737a91d02 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,45 +1,45 @@ -var apps = {}; -var stats = {}; -var isLoaded = false; -var users = {}; +export var apps = {}; +export var stats = {}; +export var isLoaded = false; +export var users = {}; -function getApp(app, callback) { +export function getApp(app, callback) { if (apps[app]) return callback(true, apps[app]); return callback(false); } -function updateStat(key, value) { +export function updateStat(key, value) { stats[key] = value; } -function getUser(sessionToken) { +export function getUser(sessionToken) { if (users[sessionToken]) return users[sessionToken]; return undefined; } -function setUser(sessionToken, userObject) { +export function setUser(sessionToken, userObject) { users[sessionToken] = userObject; } -function clearUser(sessionToken) { +export function clearUser(sessionToken) { delete users[sessionToken]; } //So far used only in tests -function clearCache() { +export function clearCache() { apps = {}; stats = {}; users = {}; } -module.exports = { - apps: apps, - stats: stats, - isLoaded: isLoaded, - getApp: getApp, - updateStat: updateStat, - clearUser: clearUser, - getUser: getUser, - setUser: setUser, - clearCache: clearCache, +export default { + apps, + stats, + isLoaded, + getApp, + updateStat, + clearUser, + getUser, + setUser, + clearCache, }; diff --git a/src/index.js b/src/index.js index 6b48018e2e..dd2cbcce1a 100644 --- a/src/index.js +++ b/src/index.js @@ -4,13 +4,13 @@ import 'babel-polyfill'; var batch = require('./batch'), bodyParser = require('body-parser'), - cache = require('./cache'), DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse; +import cache from './cache'; import PromiseRouter from './PromiseRouter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { S3Adapter } from './Adapters/Files/S3Adapter'; diff --git a/src/middlewares.js b/src/middlewares.js index 6efaabd958..e6fcc9a992 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,7 +1,8 @@ +import cache from './cache'; + var Parse = require('parse/node').Parse; var auth = require('./Auth'); -var cache = require('./cache'); var Config = require('./Config'); // Checks that the request is authorized for this app and checks user diff --git a/src/rest.js b/src/rest.js index 35fce41501..5a104c72a2 100644 --- a/src/rest.js +++ b/src/rest.js @@ -8,8 +8,8 @@ // things. var Parse = require('parse/node').Parse; +import cache from './cache'; -var cache = require('./cache'); var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); diff --git a/src/testing-routes.js b/src/testing-routes.js index 28b02cf444..3823946faf 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -1,7 +1,7 @@ // testing-routes.js +import cache from './cache'; var express = require('express'), - cache = require('./cache'), middlewares = require('./middlewares'), cryptoUtils = require('./cryptoUtils'); diff --git a/src/triggers.js b/src/triggers.js index 486b3f78ed..831f345217 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,49 +1,56 @@ // triggers.js -var Parse = require('parse/node').Parse, - cache = require('./cache'); +import Parse from 'parse/node'; +import cache from './cache'; -var Types = { +export const Types = { beforeSave: 'beforeSave', afterSave: 'afterSave', beforeDelete: 'beforeDelete', afterDelete: 'afterDelete' }; -var BaseStore = function() { - this.Functions = {} - this.Validators = {} - this.Triggers = Object.keys(Types).reduce(function(base, key){ +const baseStore = function() { + + let Validators = {}; + let Functions = {}; + let Triggers = Object.keys(Types).reduce(function(base, key){ base[key] = {}; return base; }, {}); + + return Object.freeze({ + Functions, + Validators, + Triggers + }); } -var _triggerStore = {}; +const _triggerStore = {}; -function addFunction(functionName, handler, validationHandler, applicationId) { +export function addFunction(functionName, handler, validationHandler, applicationId) { applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || new BaseStore(); + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); _triggerStore[applicationId].Functions[functionName] = handler; _triggerStore[applicationId].Validators[functionName] = validationHandler; } -function addTrigger(type, className, handler, applicationId) { +export function addTrigger(type, className, handler, applicationId) { applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || new BaseStore(); + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); _triggerStore[applicationId].Triggers[type][className] = handler; } -function removeFunction(functionName, applicationId) { +export function removeFunction(functionName, applicationId) { applicationId = applicationId || Parse.applicationId; delete _triggerStore[applicationId].Functions[functionName] } -function removeTrigger(type, className, applicationId) { +export function removeTrigger(type, className, applicationId) { applicationId = applicationId || Parse.applicationId; delete _triggerStore[applicationId].Triggers[type][className] } -function _unregister(a,b,c,d) { +export function _unregister(a,b,c,d) { if (d) { removeTrigger(c,d,a); delete _triggerStore[a][b][c][d]; @@ -53,7 +60,7 @@ function _unregister(a,b,c,d) { } -var getTrigger = function(className, triggerType, applicationId) { +export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw "Missing ApplicationID"; } @@ -67,11 +74,11 @@ var getTrigger = function(className, triggerType, applicationId) { return undefined; }; -function triggerExists(className: string, type: string): boolean { - return (getTrigger(className, type) != undefined); +export function triggerExists(className: string, type: string, applicationId: string): boolean { + return (getTrigger(className, type, applicationId) != undefined); } -var getFunction = function(functionName, applicationId) { +export function getFunction(functionName, applicationId) { var manager = _triggerStore[applicationId]; if (manager && manager.Functions) { return manager.Functions[functionName]; @@ -79,7 +86,7 @@ var getFunction = function(functionName, applicationId) { return undefined; } -var getValidator = function(functionName, applicationId) { +export function getValidator(functionName, applicationId) { var manager = _triggerStore[applicationId]; if (manager && manager.Validators) { return manager.Validators[functionName]; @@ -87,7 +94,7 @@ var getValidator = function(functionName, applicationId) { return undefined; } -var getRequestObject = function(triggerType, auth, parseObject, originalParseObject) { +export function getRequestObject(triggerType, auth, parseObject, originalParseObject) { var request = { triggerName: triggerType, object: parseObject, @@ -116,7 +123,7 @@ var getRequestObject = function(triggerType, auth, parseObject, originalParseObj // The API will call this with REST API formatted objects, this will // transform them to Parse.Object instances expected by Cloud Code. // Any changes made to the object in a beforeSave will be included. -var getResponseObject = function(request, resolve, reject) { +export function getResponseObject(request, resolve, reject) { return { success: function(response) { // Use the JSON response @@ -141,7 +148,7 @@ var getResponseObject = function(request, resolve, reject) { // 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. // originalParseObject is optional, we only need that for befote/afterSave functions -var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObject, applicationId) { +export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, applicationId) { if (!parseObject) { return Promise.resolve({}); } @@ -160,38 +167,10 @@ var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObje // Converts a REST-format object to a Parse.Object // data is either className or an object -function inflate(data, restObject) { +export function inflate(data, restObject) { var copy = typeof data == 'object' ? data : {className: data}; for (var key in restObject) { copy[key] = restObject[key]; } return Parse.Object.fromJSON(copy); } - -<<<<<<< 5fae41183ed476976ff29a4c247aa78b00b83a9e -module.exports = { - getTrigger: getTrigger, - getRequestObject: getRequestObject, - inflate: inflate, - maybeRunTrigger: maybeRunTrigger, - triggerExists: triggerExists, - 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 From 3c6141576f72d3d76d30fac9b61dbb590e0abc55 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 25 Feb 2016 12:59:19 -0500 Subject: [PATCH 4/4] Fixes --- src/Controllers/HooksController.js | 27 ++++++++++++++------------- src/index.js | 4 ++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 9de061bfbf..d059396afb 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -1,5 +1,6 @@ var DatabaseAdapter = require('../DatabaseAdapter'), - triggers = require('../triggers'); + triggers = require('../triggers'), + request = require('request'); const collection = "_Hooks"; export class HooksController { @@ -183,18 +184,18 @@ export class HooksController { } function wrapToHTTPRequest(hook) { - return function(request, response) { + return function(req, res) { var jsonBody = {}; - for(var i in request) { - jsonBody[i] = request[i]; + for(var i in req) { + jsonBody[i] = req[i]; } - if (request.object) { - jsonBody.object = request.object.toJSON(); - jsonBody.object.className = request.object.className; + if (req.object) { + jsonBody.object = req.object.toJSON(); + jsonBody.object.className = req.object.className; } - if (request.original) { - jsonBody.original = request.original.toJSON(); - jsonBody.original.className = request.original.className; + if (req.original) { + jsonBody.original = req.original.toJSON(); + jsonBody.original.className = req.original.className; } var jsonRequest = {}; jsonRequest.headers = { @@ -202,7 +203,7 @@ function wrapToHTTPRequest(hook) { } jsonRequest.body = JSON.stringify(jsonBody); - require("request").post(hook.url, jsonRequest, function(err, res, body){ + request.post(hook.url, jsonRequest, function(err, httpResponse, body){ var result; if (body) { if (typeof body == "string") { @@ -218,9 +219,9 @@ function wrapToHTTPRequest(hook) { } } if (err) { - return response.error(err); + return res.error(err); } else { - return response.success(result); + return res.success(result); } }); } diff --git a/src/index.js b/src/index.js index dd2cbcce1a..686e9c6965 100644 --- a/src/index.js +++ b/src/index.js @@ -91,8 +91,8 @@ function ParseServer({ }) { // Initialize the node client SDK automatically - Parse.initialize(appId, javascriptKey || '', masterKey); - Parse.serverURL = serverURL || ''; + Parse.initialize(appId, javascriptKey, masterKey); + Parse.serverURL = serverURL; if (databaseAdapter) { DatabaseAdapter.setAdapter(databaseAdapter);