From 1796f5e95ff73a0ec02f778a9e44d40645afc9ea Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 5 Feb 2016 14:38:09 -0500 Subject: [PATCH 1/2] adds support for multiple apps on a single server - Adds support for loading configuration from JSON - Adds support for loading multiple configurations from ENV - Adds generator for new app keys Better implementation of cloud based hooks/triggers Adds Cloud Code as subprocess - Adds basic hook registration handlers - Adds ability to isolate cloud code process Adds full hooks API Adds JSON storage controller and refactors Hooks around it Improves coverage for hooks Adds master key requirement when registering hooks Adds hooks creation strategy, improves hooks API - Fixes critical bug in overriding cloud code functions Bumps parse-cloud-express to 1.2.0 - Uses httpRequest from parse-cloud-express Improves documentation for Cloud Code Launch Cloud Code with forever Isolation of Multi server tests --- .gitignore | 6 + README.md | 91 ++++++- bin/config.js | 62 +++++ bin/gen-keys | 15 ++ bin/parse-server | 36 +-- package.json | 3 + spec/ConfigurationLoading.spec.js | 88 +++++++ spec/ParseACL.spec.js | 6 +- spec/ParseAPI.spec.js | 37 ++- spec/ParseHooks.spec.js | 233 +++++++++++++++++ spec/ParseServer-Cloud.spec.js | 313 +++++++++++++++++++++++ spec/helper.js | 9 +- spec/support/parse-server-config.json | 24 ++ src/Config.js | 3 +- src/Controllers/FilesController.js | 34 ++- src/Controllers/HooksFileCache.js | 73 ++++++ src/Controllers/JSONStorageController.js | 49 ++++ src/Controllers/PushController.js | 6 +- src/RestWrite.js | 6 +- src/cloud-code/Parse.Cloud.js | 110 ++++++++ src/cloud-code/Parse.Hooks.js | 132 ++++++++++ src/cloud-code/README.md | 23 ++ src/cloud-code/index.js | 44 ++++ src/cloud-code/launcher.js | 28 ++ src/cloud/main-2.js | 3 + src/cloud/main.js | 2 - src/functions.js | 21 +- src/hooks.js | 205 +++++++++++++++ src/httpRequest.js | 43 ---- src/index.js | 190 ++++++++------ src/middlewares.js | 3 +- src/rest.js | 12 +- src/triggers.js | 118 +++++++-- 33 files changed, 1814 insertions(+), 214 deletions(-) create mode 100644 bin/config.js create mode 100755 bin/gen-keys create mode 100644 spec/ConfigurationLoading.spec.js create mode 100644 spec/ParseHooks.spec.js create mode 100644 spec/ParseServer-Cloud.spec.js create mode 100644 spec/support/parse-server-config.json create mode 100644 src/Controllers/HooksFileCache.js create mode 100644 src/Controllers/JSONStorageController.js create mode 100644 src/cloud-code/Parse.Cloud.js create mode 100644 src/cloud-code/Parse.Hooks.js create mode 100644 src/cloud-code/README.md create mode 100644 src/cloud-code/index.js create mode 100644 src/cloud-code/launcher.js create mode 100644 src/cloud/main-2.js create mode 100644 src/hooks.js delete mode 100644 src/httpRequest.js diff --git a/.gitignore b/.gitignore index 318fed2034..a2522c164c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,11 @@ node_modules # WebStorm/IntelliJ .idea +# visual studio code +.vscode + # Babel.js lib/ + +# Cache +.cache diff --git a/README.md b/README.md index b1cfba4054..7600c18e3f 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,6 @@ var ParseServer = require('parse-server').ParseServer; var app = express(); -var port = process.env.PORT || 1337; - // Specify the connection string for your mongodb database // and the location to your Parse cloud code var api = new ParseServer({ @@ -138,6 +136,7 @@ app.get('/', function(req, res) { res.status(200).send('Express is running here.'); }); +var port = process.env.PORT || 1337; app.listen(port, function() { console.log('parse-server-example running on port ' + port + '.'); }); @@ -171,6 +170,86 @@ Alernatively, you can use the `PARSE_SERVER_OPTIONS` environment variable set to To start the server, just run `npm start`. + +#### Configuration file + +You can pass a configuration JSON file to npm start: + +`$ npm start -- --config path/to/config.json` + +(note that the first `--` is the required format by npm) + +#### Multiple applications + +You can host mutiple applications on the same server by specifying as options or use a config JSON; + +``` +{ + "applications": [ + { + "appId": "APP1", + "masterKey": "MASTERKEY1", + ... + }, + { + "appId": "APP2", + "masterKey": "MASTERKEY2", + ... + }, + // General adapters configuration (optional) + // It's overriden by specific configuration + databaseAdapter: "...", + filesAdatpter: "..." + ] +} +``` + +Use `$ npm start -- --config path/to/config.json` to start the server + + +:+1: if you use the `PARSE_SERVER_OPTIONS` environment variable, the multiple applications support will be granted too. + +:warning: Make sure to use different databases for each app. The behaviour could be unexpected otherwise. + +##### Cloud Code for multiple applications + +Cloud code will run in a separate node process and use HTTP as a transport to register the hooks. + +``` +cloud: "path/to/main.js" +``` + +The cloud code server will start on port 8081 and will be incremented for each app. + + +You can specify a specific port for each of your cloud code: + +``` +cloud: { + main: "/path/to/main.js", + port: 12345, + forever: { + ... // (Options to pass to forever)[https://github.com/foreverjs/forever-monitor] + } +} +``` + +If you only have a single app, but pass an object for the cloud option, +this will be run in a separate process too. + +The other options available for Cloud Code are: + +`hooksCreationStrategy: "always" | "never" | "try"` + +* *always* will always use the last cloud code server +* *never* will not register the new hook +* *try* will register the hook if it doesn't exist + +##### Standalone Cloud Code Server + +please see (here)[https://github.com/ParsePlatform/parse-server/blob/master/src/cloud-code/README.md] + + ##### Global installation You can install parse-server globally @@ -179,6 +258,14 @@ You can install parse-server globally Now you can just run `$ parse-server` from your command line. +To pass a configuration file you can use `$ parse-server --config path/to/config.json` + + +#### Create a new set of keys + +run `$ ./bin/gen-keys` to generate a new set of keys for a new app. + +You can use the configuration provided with the json configuration. ### Supported diff --git a/bin/config.js b/bin/config.js new file mode 100644 index 0000000000..66a00a1e38 --- /dev/null +++ b/bin/config.js @@ -0,0 +1,62 @@ +var path = require("path"); +function loadFromCommandLine(args) { + args = args || []; + while (args.length > 0) { + if (args[0] == "--config") { + if (args.length < 2) { + throw "Please specify the configuration file (json)"; + } + return require(path.resolve(args[1])); + } + args = args.slice(1, args.length); + } +} + +function loadFromEnvironment(env) { + env = env || {}; + var options = {}; + if (env.PARSE_SERVER_OPTIONS) { + + options = JSON.parse(env.PARSE_SERVER_OPTIONS); + + } else { + + options.databaseURI = env.PARSE_SERVER_DATABASE_URI; + options.cloud = env.PARSE_SERVER_CLOUD_CODE_MAIN; + options.collectionPrefix = env.PARSE_SERVER_COLLECTION_PREFIX; + + // Keys and App ID + options.appId = env.PARSE_SERVER_APPLICATION_ID; + options.clientKey = env.PARSE_SERVER_CLIENT_KEY; + options.restAPIKey = env.PARSE_SERVER_REST_API_KEY; + options.dotNetKey = env.PARSE_SERVER_DOTNET_KEY; + options.javascriptKey = env.PARSE_SERVER_JAVASCRIPT_KEY; + options.dotNetKey = env.PARSE_SERVER_DOTNET_KEY; + options.masterKey = env.PARSE_SERVER_MASTER_KEY; + options.fileKey = env.PARSE_SERVER_FILE_KEY; + // Comma separated list of facebook app ids + var facebookAppIds = env.PARSE_SERVER_FACEBOOK_APP_IDS; + + if (facebookAppIds) { + facebookAppIds = facebookAppIds.split(","); + options.facebookAppIds = facebookAppIds; + } + var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS; + if (oauth) { + options.oauth = JSON.parse(oauth); + }; + } + return options; +} + + +module.exports = function() { + var options = loadFromCommandLine(process.argv); + if (typeof options == "undefined") { + options = loadFromEnvironment(process.env); + } + return options; +} + +module.exports.loadFromEnvironment = loadFromEnvironment; +module.exports.loadFromCommandLine = loadFromCommandLine; \ No newline at end of file diff --git a/bin/gen-keys b/bin/gen-keys new file mode 100755 index 0000000000..0a6ca58746 --- /dev/null +++ b/bin/gen-keys @@ -0,0 +1,15 @@ +#!/usr/bin/env node +var rack = require('hat').rack(); + +var newApp = { + "appId": rack(), + "masterKey": rack(), + "clientKey": rack(), + "javascriptKey": rack(), + "dotNetKey": rack(), + "restAPIKey": rack(), + "collectionPrefix": "", + "databaseURI": "" +}; + +process.stdout.write(JSON.stringify(newApp, null, 4)+"\n"); \ No newline at end of file diff --git a/bin/parse-server b/bin/parse-server index 66d010414e..69a9bb248c 100755 --- a/bin/parse-server +++ b/bin/parse-server @@ -1,43 +1,13 @@ #!/usr/bin/env node +var path = require("path"); var express = require('express'); var ParseServer = require("../lib/index").ParseServer; +var options = require("./config")(); var app = express(); -var options = {}; -if (process.env.PARSE_SERVER_OPTIONS) { - - options = JSON.parse(process.env.PARSE_SERVER_OPTIONS); - -} else { - - options.databaseURI = process.env.PARSE_SERVER_DATABASE_URI; - options.cloud = process.env.PARSE_SERVER_CLOUD_CODE_MAIN; - options.collectionPrefix = process.env.PARSE_SERVER_COLLECTION_PREFIX; - - // Keys and App ID - options.appId = process.env.PARSE_SERVER_APPLICATION_ID; - options.clientKey = process.env.PARSE_SERVER_CLIENT_KEY; - options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY; - options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY; - options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY; - options.masterKey = process.env.PARSE_SERVER_MASTER_KEY; - options.fileKey = process.env.PARSE_SERVER_FILE_KEY; - // Comma separated list of facebook app ids - var facebookAppIds = process.env.PARSE_SERVER_FACEBOOK_APP_IDS; - - if (facebookAppIds) { - facebookAppIds = facebookAppIds.split(","); - options.facebookAppIds = facebookAppIds; - } - - var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS; - if (oauth) { - options.oauth = JSON.parse(oauth); - }; -} - var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/"; + var api = new ParseServer(options); app.use(mountPath, api); diff --git a/package.json b/package.json index a39b5ca1b6..52e222bb46 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,14 @@ "body-parser": "^1.14.2", "deepcopy": "^0.6.1", "express": "^4.13.4", + "forever-monitor": "^1.7.0", "mime": "^1.3.4", "mongodb": "~2.1.0", "multer": "^1.1.0", "node-gcm": "^0.14.0", "parse": "^1.7.0", + "parse-cloud-express": "~1.2.0", + "randomstring": "^1.1.3", "request": "^2.65.0", "winston": "^2.1.1" }, diff --git a/spec/ConfigurationLoading.spec.js b/spec/ConfigurationLoading.spec.js new file mode 100644 index 0000000000..5884c30b58 --- /dev/null +++ b/spec/ConfigurationLoading.spec.js @@ -0,0 +1,88 @@ +var configuration = require("./support/parse-server-config.json"); +var Parse = require("parse/node"); +var apps = configuration.applications; +var configLoader = require("../bin/config"); + +describe('Configuration loading', () => { + + it('should load a JSON from arguments', done => { + var config = configLoader.loadFromCommandLine(["--config", "./spec/support/parse-server-config.json"]); + expect(config).not.toBe(undefined); + expect(config.applications.length).toBe(2); + done(); + }); + + it('should throw when json does not exist', done => { + function load() { + return configLoader.loadFromCommandLine(["--config", "./spec/support/bar.json"]); + } + expect(load).toThrow(); + done(); + }); + + it('should throw when json is missing', done => { + function load() { + return configLoader.loadFromCommandLine(["--config"]); + } + expect(load).toThrow("Please specify the configuration file (json)"); + done(); + }); + + it('should retun nothing when nothing is specified', done => { + var config = configLoader.loadFromCommandLine(); + expect(config).toBe(undefined); + done(); + }); + + it('should support more arguments', done => { + var config = configLoader.loadFromCommandLine(["--some","--config", "./spec/support/parse-server-config.json", "--other"]); + expect(config).not.toBe(undefined); + expect(config.applications.length).toBe(2); + done(); + }); + + it('should load from environment', done => { + var env = { + PARSE_SERVER_DATABASE_URI: "", + PARSE_SERVER_CLOUD_CODE_MAIN: "", + PARSE_SERVER_COLLECTION_PREFIX: "", + PARSE_SERVER_APPLICATION_ID: "", + PARSE_SERVER_CLIENT_KEY: "", + PARSE_SERVER_REST_API_KEY: "", + PARSE_SERVER_DOTNET_KEY: "", + PARSE_SERVER_JAVASCRIPT_KEY: "", + PARSE_SERVER_DOTNET_KEY: "", + PARSE_SERVER_MASTER_KEY: "", + PARSE_SERVER_FILE_KEY: "", + PARSE_SERVER_FACEBOOK_APP_IDS: "hello,world" + } + + var config = configLoader.loadFromEnvironment(env); + expect(config).not.toBe(undefined); + expect(Object.keys(config).length).toBe(Object.keys(env).length); + expect(config.facebookAppIds.length).toBe(2); + expect(config.facebookAppIds).toContain("hello"); + expect(config.facebookAppIds).toContain("world"); + done(); + }); + + it('should load from environment options', done => { + var env = { + PARSE_SERVER_OPTIONS: require("fs").readFileSync("./spec/support/parse-server-config.json") + } + + var config = configLoader.loadFromEnvironment(env); + expect(config).not.toBe(undefined); + expect(config.applications.length).toBe(2); + done(); + }); + + it('should load empty configuration options', done => { + var config = configLoader(); + expect(config).not.toBe(undefined); + expect(config).not.toBe({}); + expect(config.appId).toBe(undefined); + done(); + }); + +}); \ No newline at end of file diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 370550c0a5..8b8869ed20 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 52c17fbfed..4ec821eccd 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -274,7 +274,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 @@ -350,6 +354,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'); @@ -380,7 +389,10 @@ describe('miscellaneous', function() { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); done(); - }); + }).fail( err => { + fail(err); + done(); + }) }); it('test beforeSave get full object on create and update', function(done) { @@ -417,7 +429,8 @@ describe('miscellaneous', function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); // Clear mock beforeSave - delete Parse.Cloud.Triggers.beforeSave.GameScore; + + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, function(error) { fail(error); @@ -459,9 +472,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(); }); @@ -511,7 +525,7 @@ describe('miscellaneous', function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); // Clear mock beforeSave - delete Parse.Cloud.Triggers.beforeSave.GameScore; + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, function(error) { fail(error); @@ -563,9 +577,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(); }); @@ -578,12 +593,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(); }); }); @@ -612,7 +627,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(); }); }); @@ -626,7 +641,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.'); @@ -644,7 +659,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..19fca2c4b6 --- /dev/null +++ b/spec/ParseHooks.spec.js @@ -0,0 +1,233 @@ +/* global describe, it, expect, fail, Parse */ +var request = require('request'); +// Inject the hooks API +Parse.Hooks = require("../src/cloud-code/Parse.Hooks"); + +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-Javascript-Key": Parse.javascriptKey, + }, + body: JSON.stringify({ url: "http://hello.word", functionName: "SomeFunction"}) + }, (err, res, body) => { + body = JSON.parse(body); + expect(body.error).toBe("unauthorized"); + expect(res.statusCode).toBe(403); + 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(); + }) + }); +}); \ No newline at end of file diff --git a/spec/ParseServer-Cloud.spec.js b/spec/ParseServer-Cloud.spec.js new file mode 100644 index 0000000000..c6806721d4 --- /dev/null +++ b/spec/ParseServer-Cloud.spec.js @@ -0,0 +1,313 @@ +var configuration = require("./support/parse-server-config.json"); +var Parse = require("parse/node"); +var apps = configuration.applications; +var configLoader = require("../bin/config"); +var Server = require("../src/cloud-code"); +var jsonCacheDir = "./.cache"; +var express = require("express"); +var databaseURI = process.env.DATABASE_URI; +var ParseServer = require('../src/index').ParseServer; + + +var port = 8379; +var serverURL = 'http://localhost:' + port + '/1'; + +var app = express(); +var server = app.listen(port); + +// Set up an API server for testing +var api = new ParseServer(configuration); +app.use('/1', api); + +function createEchoHook() { + return Parse.Cloud.define("echoParseKeys", (req, res) => { + res.success({ applicationId: Parse.applicationId, + javascriptKey: Parse.javascriptKey, + masterKey: Parse.masterKey }); + }); +} + +function createBeforeSaveHook() { + return Parse.Cloud.beforeSave("InjectAppId", (req, res) => { + req.object.set('applicationId', Parse.applicationId); + req.object.set('javascriptKey', Parse.javascriptKey); + req.object.set('masterKey', Parse.masterKey); + res.success(); + }); +} + +describe('Multi Server Testing', () => { + beforeEach((done) => { + // Set the proper Pare serverURL + Parse.initialize("test2", "test2", "test2"); + Parse.serverURL = serverURL; + done(); + }) + it('first app should have hello', done => { + Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); + Parse.Cloud.run('hello', {}, (result, error) => { + expect(result).toEqual('Hello world!'); + done(); + }); + }); + + it('second app should have hello', done => { + Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + Parse.Cloud.run('hello', {}, (result, error) => { + expect(result).toEqual('Hello world'); + done(); + }); + }); + + it('should echo the right applicatio ID', done => { + Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + createEchoHook(); + Parse.Cloud.run('echoParseKeys', {}, (result, error) => { + expect(result.applicationId).toEqual(apps[1].appId); + expect(result.javascriptKey).toEqual(apps[1].javascriptKey); + expect(result.masterKey).toEqual(apps[1].masterKey); + Parse.Cloud._removeHook("Functions", 'echoParseKeys', null, apps[1].appId); + done(); + }); + + Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); + createEchoHook(); + Parse.Cloud.run('echoParseKeys', {}, (result, error) => { + expect(result.applicationId).toEqual(apps[0].appId); + expect(result.javascriptKey).toEqual(apps[0].javascriptKey); + expect(result.masterKey).toEqual(apps[0].masterKey); + Parse.Cloud._removeHook("Functions", 'echoParseKeys', null, apps[0].appId); + done(); + }); + }); + + it('should delete the proper hook and not leak', done => { + + Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + createEchoHook(); + + Parse.Cloud.run('echoParseKeys', {}).then( (result) => { + expect(result.applicationId).toEqual(apps[1].appId); + expect(result.javascriptKey).toEqual(apps[1].javascriptKey); + expect(result.masterKey).toEqual(apps[1].masterKey); + Parse.Cloud._removeHook("Functions", 'echoParseKeys'); + return Parse.Promise.as(); + }).then( () => { + Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); + return Parse.Cloud.run('echoParseKeys', {}); + }).then( (res) => { + fail("this call should not succeed"); + done(); + }).fail( (err) => { + expect(err.code).toEqual(141); + done(); + }); + + }); + + it('should create the proper beforeSave and set the proper app ID', done => { + + Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + createBeforeSaveHook(); + var obj = new Parse.Object('InjectAppId'); + obj.save().then( () => { + var query = new Parse.Query('InjectAppId'); + query.get(obj.id).then( (objAgain) => { + expect(objAgain.get('applicationId')).toEqual(apps[1].appId); + expect(objAgain.get('javascriptKey')).toEqual(apps[1].javascriptKey); + expect(objAgain.get('masterKey')).toEqual(apps[1].masterKey); + Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); + done(); + }, (error) => { + fail(error); + Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); + done(); + }); + }, (error) => { + fail(error); + Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); + done(); + }); + + }); + + it('should create an object in the proper DB (and not the other)', done => { + + Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + var obj = new Parse.Object('SomeObject'); + obj.save().then( () => { + var query = new Parse.Query('SomeObject'); + return query.get(obj.id); + }, (error) => { + fail(error); + done(); + }).then( (objAgain) => { + + expect(objAgain).not.toBeUndefined(); + expect(objAgain.id).toEqual(obj.id); + + // Check if the data exists in another app + Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); + var q = new Parse.Query('SomeObject'); + return q.find(); + + }, (error) => { + fail(error); + done(); + }).then( (result) => { + expect(result.constructor).toBe(Array.prototype.constructor); + expect(result.length).toBe(0); + done(); + }, (error) => { + fail(error); + done(); + }); + + }); + + it('should create a proper cloud code server for an existing parse APP', done => { + // Start a cloud code server for APP 1. + var config = { + applicationId: apps[1].appId, + javascriptKey: apps[1].javascriptKey, + masterKey: apps[1].masterKey, + port: 12345, + main: "../cloud/main-2.js", + serverURL: Parse.serverURL, + hooksCreationStrategy: "always" + }; + var server = new Server(config); + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + Parse.Cloud.define("myCloud", (req, res) => { + res.success("code!"); + }).then( () => { + Parse.Cloud.run("myCloud", {}, (result, error) => { + if (error) { + fail(error); + } + expect(result).toEqual('code!'); + server.close(); + done(); + }); + }, (err) => { + fail(err); + server.close(); + done(); + }); + + }); + + it('test beforeSave on custom Cloud Code (create update)', (done) => { + + // Start a cloud code server for APP 1. + var config = { + applicationId: apps[1].appId, + javascriptKey: apps[1].javascriptKey, + masterKey: apps[1].masterKey, + port: 12345, + main: "../cloud/main.js", + serverURL: Parse.serverURL, + hooksCreationStrategy: "always" + }; + var server = new Server(config); + + var triggerTime = 0; + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + // Register a mock beforeSave hook + Parse.Cloud.beforeSave('GameScore', (req, res) => { + var object = req.object; + // TODO: The Parse objects are different in CC execution + // Because it comes from parse-cloud-express + // expect(object instanceof Parse.Object).toBeTruthy(); + expect(object.get('fooAgain')).toEqual('barAgain'); + expect(object.id).not.toBeUndefined(); + expect(object.createdAt).not.toBeUndefined(); + expect(object.updatedAt).not.toBeUndefined(); + if (triggerTime == 0) { + // Create + expect(object.get('foo')).toEqual('bar'); + } else if (triggerTime == 1) { + // Update + expect(object.get('foo')).toEqual('baz'); + } else { + res.error(); + } + triggerTime++; + res.success(); + }).then( () => { + var obj = new Parse.Object('GameScore'); + obj.set('foo', 'bar'); + obj.set('fooAgain', 'barAgain'); + obj.save().then( () => { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }).then( () => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + // Clear mock beforeSave + if (Parse.Cloud._removeHook) { + Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); + }; + server.close(); + done(); + }, (error) => { + fail(error); + server.close(); + done(); + }); + }, (err) => { + fail(err); + server.close(); + done(); + }); + + }); + + it('should not create the hook', (done) => { + + // Start a cloud code server for APP 1. + var config = { + applicationId: apps[1].appId, + javascriptKey: apps[1].javascriptKey, + masterKey: apps[1].masterKey, + port: 12345, + main: "../cloud/main.js", + serverURL: Parse.serverURL, + hooksCreationStrategy: "always" + }; + var server = new Server(config); + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + + Parse.Cloud.define("hello_world", (req, res) => { + + fail("This shoud not be called!"); + res.success("Hello!"); + + }, "never") + .then( res => { + + expect(res).toBeUndefined(); + return Parse.Cloud.run("hello_world", {}); + + }).then( (res) => { + + expect(res).toBeUndefined(); + fail("Should not be defined"); + server.close(); + done(); + + }, (err) => { + + expect(err).not.toBeUndefined(); + expect(err.code).toBe(141); + expect(err.message).toBe('Invalid function.'); + server.close(); + done(); + + }); + }); +}); diff --git a/spec/helper.js b/spec/helper.js index 8b587f7d5d..a9ef869977 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -10,7 +10,8 @@ var ParseServer = require('../src/index').ParseServer; var databaseURI = process.env.DATABASE_URI; var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js'; - +var port = 8378; +var serverURL = 'http://localhost:' + port + '/1'; // Set up an API server for testing var api = new ParseServer({ databaseURI: databaseURI, @@ -33,12 +34,11 @@ var api = new ParseServer({ var app = express(); app.use('/1', api); -var port = 8378; + var server = app.listen(port); // Set up a Parse client to talk to our test API server var Parse = require('parse/node'); -Parse.serverURL = 'http://localhost:' + port + '/1'; // This is needed because we ported a bunch of tests from the non-A+ way. // TODO: update tests to work in an A+ way @@ -46,6 +46,8 @@ Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { Parse.initialize('test', 'test', 'test'); + Parse.serverURL = serverURL; + mockFacebook(); Parse.User.enableUnsafeCurrentUser(); done(); }); @@ -221,3 +223,4 @@ global.expectError = expectError; global.arrayContains = arrayContains; global.jequal = jequal; global.range = range; +global.mockFacebook = mockFacebook; \ No newline at end of file diff --git a/spec/support/parse-server-config.json b/spec/support/parse-server-config.json new file mode 100644 index 0000000000..f6f74ed638 --- /dev/null +++ b/spec/support/parse-server-config.json @@ -0,0 +1,24 @@ +{ + "applications": [ + { + "appId": "test2", + "javascriptKey": "test2", + "dotNetKey": "windows", + "clientKey": "client", + "restAPIKey": "rest", + "masterKey": "test2", + "collectionPrefix": "test_", + "fileKey": "test", + "databaseURI": "mongodb://localhost:27017/test2", + "cloud" : "./src/cloud/main.js" + }, + { + "appId": "multi-server-test", + "javascriptKey": "1234", + "masterKey": "testMasterKey", + "cloud" : "./src/cloud/main-2.js", + "databaseURI": "mongodb://localhost:27017/test-altername", + "fileKey": "multi-test" + } + ] +} diff --git a/src/Config.js b/src/Config.js index aeb25a6173..3b9188a483 100644 --- a/src/Config.js +++ b/src/Config.js @@ -24,8 +24,9 @@ function Config(applicationId, mount) { this.database = DatabaseAdapter.getDatabaseConnection(applicationId); this.filesController = cacheInfo.filesController; - + this.pushController = cacheInfo.pushController; this.oauth = cacheInfo.oauth; + this.mount = mount; } diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 6fde54b766..f1238694aa 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -13,6 +13,13 @@ export class FilesController { this._filesAdapter = filesAdapter; } + static getHandler() { + return (req, res) => { + let config = new Config(req.params.appId); + return config.filesController.getHandler()(req, res); + } + } + getHandler() { return (req, res) => { let config = new Config(req.params.appId); @@ -30,6 +37,13 @@ export class FilesController { }; } + static createHandler() { + return (req, res, next) => { + let config = req.config; + return config.filesController.createHandler()(req, res, next); + } + } + createHandler() { return (req, res, next) => { if (!req.body || !req.body.length) { @@ -50,6 +64,7 @@ export class FilesController { return; } + const filesController = req.config.filesController; // If a content-type is included, we'll add an extension so we can // return the same content-type. let extension = ''; @@ -60,9 +75,9 @@ export class FilesController { } let filename = randomHexString(32) + '_' + req.params.filename + extension; - this._filesAdapter.createFile(req.config, filename, req.body).then(() => { + filesController._filesAdapter.createFile(req.config, filename, req.body).then(() => { res.status(201); - var location = this._filesAdapter.getFileLocation(req.config, filename); + var location = filesController._filesAdapter.getFileLocation(req.config, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { @@ -72,6 +87,13 @@ export class FilesController { }; } + static deleteHandler() { + return (req, res, next) => { + let config = req.config; + return config.filesController.deleteHandler()(req, res, next); + } + } + deleteHandler() { return (req, res, next) => { this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => { @@ -114,9 +136,9 @@ export class FilesController { } } - getExpressRouter() { + static getExpressRouter() { let router = express.Router(); - router.get('/files/:appId/:filename', this.getHandler()); + router.get('/files/:appId/:filename', FilesController.getHandler()); router.post('/files', function(req, res, next) { next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, @@ -127,14 +149,14 @@ export class FilesController { Middlewares.allowCrossDomain, BodyParser.raw({type: '*/*', limit: '20mb'}), Middlewares.handleParseHeaders, - this.createHandler() + FilesController.createHandler() ); router.delete('/files/:filename', Middlewares.allowCrossDomain, Middlewares.handleParseHeaders, Middlewares.enforceMasterKeyAccess, - this.deleteHandler() + FilesController.deleteHandler() ); return router; diff --git a/src/Controllers/HooksFileCache.js b/src/Controllers/HooksFileCache.js new file mode 100644 index 0000000000..06aca1322f --- /dev/null +++ b/src/Controllers/HooksFileCache.js @@ -0,0 +1,73 @@ +import { JSONStorageProvider } from './JSONStorageController'; + +export class HooksFileCache { + constructor(appId) { + this.appId = appId; + this.fileName = "hooks-"+this.appId+".json"; + } + + addHook(hook) { + var json = this.getHooks(); + if (hook.triggerName) { + json.triggers[hook.triggerName+"_"+hook.className] = hook; + } else { + json.functions[hook.functionName] = hook; + } + this.saveHooks(json); + } + + saveHooks(json) { + JSONStorageProvider.getAdapter().write(this.fileName, json, this.appId); + } + + getHooks() { + var json = JSONStorageProvider.getAdapter().read(this.fileName, this.appId); + json.triggers = json.triggers || {}; + json.functions = json.functions || {}; + return json; + } + + getFunction(functionName) { + return this.getHooks().functions[functionName]; + + } + + getTrigger(className, triggerName) { + var triggersMap = this.getHooks().triggers; + return triggersMap[`${triggerName}_${className}`]; + } + + getTriggers() { + var triggersMap = this.getHooks().triggers; + return Object.keys(triggersMap).map(function(key){ + return triggersMap[key]; + }); + } + + getFunctions() { + var functions = this.getHooks().functions; + return Object.keys(functions).map(function(key){ + return functions[key]; + }); + } + + removeHook(functionName, triggerName = null) { + var hooks = this.getHooks(); + var changed = false; + if (!triggerName) { + if (hooks.functions[functionName]) { + delete hooks.functions[functionName]; + changed = true; + } + } else { + if (hooks.triggers[triggerName+"_"+functionName]) { + delete hooks.triggers[triggerName+"_"+functionName]; + changed = true; + } + } + if (changed) { + this.saveHooks(hooks) + } + return changed; + } +} diff --git a/src/Controllers/JSONStorageController.js b/src/Controllers/JSONStorageController.js new file mode 100644 index 0000000000..39df8e8436 --- /dev/null +++ b/src/Controllers/JSONStorageController.js @@ -0,0 +1,49 @@ +const path = require("path"), + fs = require("fs"); + +export class JSONStorageController { + + constructor(basePath = null) { + this.basePath = basePath; + } + + getDirectoryForAppId(appId) { + var dir = this.basePath+"/"+appId; + dir = path.resolve(dir); + try { + fs.statSync(this.basePath); + } catch(e) { + fs.mkdir(this.basePath); + } + try { + fs.statSync(dir); + } catch(e) { + fs.mkdir(dir); + } + return dir; + } + + read(file, appId) { + var dir = this.getDirectoryForAppId(appId); + var json = {}; + try { + json = require(dir+"/"+file); + } catch (e) {} + return json; + } + + write(file, data, appId) { + var dir = this.getDirectoryForAppId(appId); + // Write sync to prevent concurrent writes on the same file + fs.writeFileSync(dir+"/"+file, JSON.stringify(data)); + } +} + +export class JSONStorageProvider { + static setAdapter(controller) { + JSONStorageProvider.adapter = controller; + } + static getAdapter() { + return JSONStorageProvider.adapter; + } +} diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 9f9252dcc8..e87e6f764c 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -31,11 +31,11 @@ export class PushController { } }); } - - getExpressRouter() { + + static getExpressRouter() { var router = new PromiseRouter(); router.route('POST','/push', (req) => { - return this.handlePOST(req); + return req.config.pushController.handlePOST(req); }); return router; } diff --git a/src/RestWrite.js b/src/RestWrite.js index 54f5cfc996..5640546780 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -112,7 +112,7 @@ RestWrite.prototype.runBeforeTrigger = function() { return Promise.resolve().then(() => { return triggers.maybeRunTrigger( - 'beforeSave', this.auth, inflatedObject, originalObject); + 'beforeSave', this.auth, inflatedObject, originalObject, this.config.applicationId); }).then((response) => { if (response && response.object) { this.data = response.object; @@ -260,7 +260,7 @@ RestWrite.prototype.handleOAuthAuthData = function(provider) { if (!validateAuthData || !validateAppId) { return false; }; - + return validateAuthData(authData, oauthOptions) .then(() => { if (appIds && typeof validateAppId === "function") { @@ -749,7 +749,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/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js new file mode 100644 index 0000000000..dd7ff3fa52 --- /dev/null +++ b/src/cloud-code/Parse.Cloud.js @@ -0,0 +1,110 @@ +var ParseCloudExpress = require("parse-cloud-express"); +var Parse = ParseCloudExpress.Parse; +Parse.Hooks = require("./Parse.Hooks") + +// Store the original Parse Cloud instance +// to prevent multiple wrapping +const PARSE_CLOUD_OVERRIDES = ["define", "beforeSave", "afterSave", "beforeDelete", "afterDelete"]; +const PARSE_CLOUD_FUNCTIONS = PARSE_CLOUD_OVERRIDES.reduce(function(a, b){ + a[b] = ParseCloudExpress.Parse.Cloud[b]; + return a; +}, {}); + +var hooksCreationStrategy = { + 'never': 'never', // never create hooks, has to manually + 'always': 'always', // try to always update the hooks (POST then PUT) + 'try': 'try', // try to create a hook, but don't override if exists +}; + +Parse.Cloud.hooksCreationStrategy = hooksCreationStrategy; + +module.exports = ParseCloudExpress.Parse.Cloud; +module.exports.injectAutoRegistration = function(config) { + + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + + var buildURL = function(name, trigger) { + trigger = trigger || "function"; + var URL = config.mountPath+"/"+trigger+"_"+name; + return URL; + } + + var registerHook = function(type, name, trigger, cloudServerURL, creationStrategy) { + + var url = ""; + var hookURL; + var data = {}; + + if (type === "function") { + url = "/hooks/functions"; + data.functionName = name; + hookURL = buildURL(name); + creationStrategy = cloudServerURL; + cloudServerURL = trigger; + } else if (type == "trigger") { + url = "/hooks/triggers"; + data.className = name; + data.triggerName = trigger; + hookURL = buildURL(name, trigger); + } + + // No creation strategy, do nothing + if (!creationStrategy || creationStrategy == hooksCreationStrategy.never) { + return Parse.Promise.as(); + } + + data.url = cloudServerURL + hookURL; + return Parse.Hooks.create(data).fail(function(err){ + if (creationStrategy == hooksCreationStrategy.always) { + return Parse.Hooks.update(data); + } + // Ignore the error then + return Parse.Promise.as(err); + }); + } + + var wrapHandler = function(handler) { + return function(request, response) { + var _success = response.success; + + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + + response.success = function(args) { + var responseValue = args; + if (request.object) { + // If the response was set with the update + // As the original API + request.object.set(args); + responseValue = {object: request.object.toJSON()}; + } + _success(responseValue); + } + + return handler(request, response); + }; + }; + + var ParseCloudOverrides = PARSE_CLOUD_OVERRIDES.reduce(function(cloud, triggerName){ + var currentTrigger = PARSE_CLOUD_FUNCTIONS[triggerName]; + cloud[triggerName] = function(name, handler, creationStrategy) { + creationStrategy = creationStrategy || config.hooksCreationStrategy; + var promise; + if (triggerName === "define") { + promise = registerHook("function", name, config.cloudServerURL, creationStrategy); + } else { + promise = registerHook("trigger", name, triggerName, config.cloudServerURL, creationStrategy); + } + if (triggerName == "beforeSave") { + handler = wrapHandler(handler); + }; + currentTrigger(name, handler); + return promise; + } + return cloud; + }, {}); + // mount the overrides on the ParseCloudExpress.Parse.Cloud + Object.assign(ParseCloudExpress.Parse.Cloud, ParseCloudOverrides); + Parse.Cloud = ParseCloudExpress.Parse.Cloud; +} 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/cloud-code/README.md b/src/cloud-code/README.md new file mode 100644 index 0000000000..59accb7356 --- /dev/null +++ b/src/cloud-code/README.md @@ -0,0 +1,23 @@ +# Standalone Cloud Code + +to create a new CloudCode server: + +``` +var CloudCodeServer = require("parse-server/lib/cloud-code"); + +var config = { + applicationId: "", + javascriptKey: "", + masterKey: "", + port: 12345, + main: "path/to/main.js", + serverURL: Parse.serverURL, // or the server URL of your parse server + hooksCreationStrategy: "always" | "try" | "never" +}; +var server = new CloudCodeServer(config); + +// From there the cloud code server started on port 12345; +server.app; // the express app running the server +server.stop() // stops the server from listening + +``` diff --git a/src/cloud-code/index.js b/src/cloud-code/index.js new file mode 100644 index 0000000000..3689f8d228 --- /dev/null +++ b/src/cloud-code/index.js @@ -0,0 +1,44 @@ +/*jshint node:true */ + +var CloudCodeServer = function(config) { + 'use strict'; + var path = require("path"); + config.cloudServerURL = config.cloudServerURL || `http://localhost:${config.port}`; + config.mountPath = config.mountPath || "/_hooks"; + var Parse = require("parse/node"); + + global.Parse = Parse; + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + var ParseCloudExpress = require('parse-cloud-express'); + require("./Parse.Cloud"); + Parse.Cloud.injectAutoRegistration(config); + + var express = require("express"); + var bodyParser = require('body-parser'); + var app = require("express/lib/application"); + + + var cloudCodeHooksApp = express(); + cloudCodeHooksApp.use(bodyParser.json({ 'type': '*/*' })); + this.httpServer = cloudCodeHooksApp.listen(config.port); + if (process.env.NODE_ENV !== "test") { + console.log("[%s] Running Cloud Code for "+Parse.applicationId+" on http://localhost:%s", process.pid, config.port); + } + + Parse.Cloud.serverURL = config.cloudServerURL; + Parse.Cloud.app = cloudCodeHooksApp; + + cloudCodeHooksApp.use(config.mountPath, ParseCloudExpress.app); + + this.app = cloudCodeHooksApp; + require(config.main); +} +CloudCodeServer.prototype.close = function() { + this.httpServer.close(); +} + +if (require.main === module) { + new CloudCodeServer(JSON.parse(process.argv[2])); +} + +module.exports = CloudCodeServer; diff --git a/src/cloud-code/launcher.js b/src/cloud-code/launcher.js new file mode 100644 index 0000000000..f8979e0aa7 --- /dev/null +++ b/src/cloud-code/launcher.js @@ -0,0 +1,28 @@ +// ignore code coverage here as it's just a process spanner +// and it gets in the way of the coverage +// And even if it fires, it doesn't get triggered as +// it is a subprocess + +/* istanbul ignore next */ +module.exports = function(options) { + var forever = require('forever-monitor'); + + var foreverOptions = Object.assign({ + max: 9999, + silent: false + }, options.forever) + + foreverOptions.env = process.env; + foreverOptions.args = [JSON.stringify(options)]; + + var cloudCode = new (forever.Monitor)(__dirname + '/index.js', foreverOptions); + + // Kill subprocess on kill + process.on('exit', () => { + cloudCode.stop(); + // Force killin! + cloudCode.child.kill('SIGHUP'); + }); + + cloudCode.start(); +} \ No newline at end of file diff --git a/src/cloud/main-2.js b/src/cloud/main-2.js new file mode 100644 index 0000000000..682202465c --- /dev/null +++ b/src/cloud/main-2.js @@ -0,0 +1,3 @@ +Parse.Cloud.define('hello', function(req, res) { + res.success('Hello world'); +}); \ No newline at end of file diff --git a/src/cloud/main.js b/src/cloud/main.js index fec259910a..9e53e6376a 100644 --- a/src/cloud/main.js +++ b/src/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/functions.js b/src/functions.js index 8e88aa0358..26b8525786 100644 --- a/src/functions.js +++ b/src/functions.js @@ -3,22 +3,27 @@ var express = require('express'), Parse = require('parse/node').Parse, PromiseRouter = require('./PromiseRouter'), - rest = require('./rest'); + rest = require('./rest'), + triggers = require("./triggers"); var router = new PromiseRouter(); function 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.'); } @@ -26,7 +31,11 @@ function handleCloudFunction(req) { return new Promise(function (resolve, reject) { var response = 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/hooks.js b/src/hooks.js new file mode 100644 index 0000000000..2638592c49 --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,205 @@ +var Parse = require('parse/node').Parse, + PromiseRouter = require('./PromiseRouter'), + triggers = require('./triggers'); + +import { HooksFileCache } from './Controllers/HooksFileCache'; + +var wrap = function(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); + } + }); + } +} + +var registerHook = function(hook, applicationId) { + var wrappedFunction = wrap(hook); + wrappedFunction.url = hook.url; + if (hook.className) { + triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, applicationId) + } else { + triggers.addFunction(hook.functionName, wrappedFunction, null, applicationId); + } + new HooksFileCache(applicationId).addHook(hook); +} + +var load = function(applicationId) { + var json = new HooksFileCache(applicationId).getHooks(); + for(var i in json.triggers) { + registerHook(json.triggers[i], applicationId); + } + for(var i in json.functions) { + registerHook(json.functions[i], applicationId); + } +}; + +var createOrUpdateHook = function(aHook, applicationId) { + if (!applicationId) { + throw "Application ID is missing"; + } + 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; + } + var promise; + if (!hook) { + promise = Promise.resolve({response: {code: 143, error: "invalid hook declaration"}}); + } else { + registerHook(aHook, applicationId); + promise = Promise.resolve({response: hook}); + } + return promise; +}; + +var createHook = function(aHook, applicationId) { + var hookCache = new HooksFileCache(applicationId); + if (aHook.functionName && hookCache.getFunction(aHook.functionName)) { + return Promise.resolve({response: {code: 143, error: `function name: ${aHook.functionName} already exits`}}); + } + if (aHook.className && aHook.triggerName && hookCache.getTrigger(aHook.className, aHook.triggerName)) { + return Promise.resolve({response: {code: 143, error: `class ${aHook.className} already has trigger ${aHook.triggerName}`}}); + } + return createOrUpdateHook(aHook, applicationId); +}; + +var updateHook = function(aHook, applicationId) { + var hookCache = new HooksFileCache(applicationId); + if (aHook.functionName && !hookCache.getFunction(aHook.functionName)) { + return Promise.resolve({response: {code: 143, error: `no function named: ${aHook.functionName} is defined`}}); + } + if (aHook.className && aHook.triggerName && !hookCache.getTrigger(aHook.className, aHook.triggerName)) { + return Promise.resolve({response: {code: 143, error: `class ${aHook.className} does not exist`}}); + } + return createOrUpdateHook(aHook, applicationId); +} + +var handlePost = function(req) { + return createHook(req.body, req.config.applicationId); +}; + +var handleGetFunctions = function(req) { + var hookCache = new HooksFileCache(req.config.applicationId); + if (req.params.functionName) { + var foundFunction = hookCache.getFunction(req.params.functionName); + if (foundFunction) { + return Promise.resolve({response: foundFunction}); + } else { + return Promise.resolve({response: {error: `no function named: ${req.params.functionName} is defined`, code: 143}}); + } + } + return Promise.resolve({ response: hookCache.getFunctions() }) +} +var handleGetTriggers = function(req) { + var hookCache = new HooksFileCache(req.config.applicationId); + if (req.params.className && req.params.triggerName) { + var foundTrigger = hookCache.getTrigger(req.params.className, req.params.triggerName); + if (foundTrigger) { + return Promise.resolve({response: foundTrigger}); + } else { + return Promise.resolve({response: {error: `class ${req.params.className} does not exist`, code: 143}}); + } + } + + return Promise.resolve({ response: hookCache.getTriggers() }) +} +var handleDelete = function(req) { + var cache = new HooksFileCache(req.config.applicationId); + if (req.params.functionName) { + triggers.removeFunction(req.params.functionName, req.config.applicationId); + cache.removeHook(req.params.functionName, req.params.triggerName) + } else if (req.params.className && req.params.triggerName) { + triggers.removeTrigger(req.params.triggerName, req.params.className,req.config.applicationId); + cache.removeHook(req.params.className, req.params.triggerName) + } + return Promise.resolve({response: {}}); +} + +var handleUpdate = function(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 + } + return updateHook(hook, req.config.applicationId); +} + +var handlePut = function(req) { + var body = req.body; + if (body.__op == "Delete") { + return handleDelete(req); + } else { + return handleUpdate(req); + } +} + +var requireMaster = function(handler) { + return (req) => { + if (req.auth.isMaster) { + return handler(req); + } + return Promise.resolve({response: {error: 'unauthorized'}, status: 403}); + } +} + +var router = new PromiseRouter(); + +router.route('GET', '/hooks/functions', requireMaster(handleGetFunctions)); +router.route('GET', '/hooks/triggers', requireMaster(handleGetTriggers)); +router.route('GET', '/hooks/functions/:functionName', requireMaster(handleGetFunctions)); +router.route('GET', '/hooks/triggers/:className/:triggerName', requireMaster(handleGetTriggers)); +router.route('POST', '/hooks/functions', requireMaster(handlePost)); +router.route('POST', '/hooks/triggers', requireMaster(handlePost)); +router.route('PUT', '/hooks/functions/:functionName', requireMaster(handlePut)); +router.route('PUT', '/hooks/triggers/:className/:triggerName', requireMaster(handlePut)); + +module.exports = router; +module.exports.load = load; diff --git a/src/httpRequest.js b/src/httpRequest.js deleted file mode 100644 index db696c65ee..0000000000 --- a/src/httpRequest.js +++ /dev/null @@ -1,43 +0,0 @@ -var request = require("request"), - Parse = require('parse/node').Parse; - -module.exports = function(options) { - var promise = new Parse.Promise(); - var callbacks = { - success: options.success, - error: options.error - }; - delete options.success; - delete options.error; - if (options.uri && !options.url) { - options.uri = options.url; - delete options.url; - } - if (typeof options.body === 'object') { - options.body = JSON.stringify(options.body); - } - request(options, (error, response, body) => { - var httpResponse = {}; - httpResponse.status = response.statusCode; - httpResponse.headers = response.headers; - httpResponse.buffer = new Buffer(response.body); - httpResponse.cookies = response.headers["set-cookie"]; - httpResponse.text = response.body; - try { - httpResponse.data = JSON.parse(response.body); - } catch (e) {} - // Consider <200 && >= 400 as errors - if (error || httpResponse.status <200 || httpResponse.status >=400) { - if (callbacks.error) { - return callbacks.error(httpResponse); - } - return promise.reject(httpResponse); - } else { - if (callbacks.success) { - return callbacks.success(httpResponse); - } - return promise.resolve(httpResponse); - } - }); - return promise; -}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 47b639f423..81f8769fb9 100644 --- a/src/index.js +++ b/src/index.js @@ -9,11 +9,16 @@ var batch = require('./batch'), multer = require('multer'), Parse = require('parse/node').Parse, PromiseRouter = require('./PromiseRouter'), - httpRequest = require('./httpRequest'); + httpRequest = require("parse-cloud-express/lib/httpRequest"), + triggers = require('./triggers'), + hooks = require('./hooks'), + path = require("path"), + CloudCodeLauncher = require("./cloud-code/launcher"); import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { S3Adapter } from './Adapters/Files/S3Adapter'; import { FilesController } from './Controllers/FilesController'; +import { JSONStorageProvider, JSONStorageController } from './Controllers/JSONStorageController'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; import { PushController } from './Controllers/PushController'; @@ -55,6 +60,88 @@ addParseCloud(); // "push": optional key from configure push function ParseServer(args) { + + loadConfiguration(args); + + // 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(); + + // File handling needs to be before default middlewares are applied + api.use(FilesController.getExpressRouter()); + + // TODO: separate this from the regular ParseServer object + if (process.env.TESTING == 1) { + console.log('enabling integration testing-routes'); + api.use('/', require('./testing-routes').router); + } + + api.use(bodyParser.json({ 'type': '*/*' })); + api.use(middlewares.allowCrossDomain); + api.use(middlewares.allowMethodOverride); + api.use(middlewares.handleParseHeaders); + + let routers = [ + new ClassesRouter().getExpressRouter(), + new UsersRouter().getExpressRouter(), + new SessionsRouter().getExpressRouter(), + new RolesRouter().getExpressRouter(), + require('./analytics'), + new InstallationsRouter().getExpressRouter(), + require('./functions'), + require('./schemas'), + require('./hooks'), + PushController.getExpressRouter() + ]; + + if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { + routers.push(require('./global_config')); + } + + let appRouter = new PromiseRouter(); + routers.forEach((router) => { + appRouter.merge(router); + }); + batch.mountOnto(appRouter); + + appRouter.mountOnto(api); + + api.use(middlewares.handleParseErrors); + + return api; +} + +function loadConfiguration(args) { + + if (args.applications) { + var port = parseInt(process.env.PORT) || 8080; + port++; + args.applications.forEach(function(app){ + if (typeof app.cloud === "string") { + app.cloud = { + main: path.resolve(app.cloud), + // Increment the port for the sub processes + port: port++, + hooksCreationStrategy: "always" + } + } + if (app.cloud) { + // Setup the defaults if needed for light cloud configurations + app.cloud.applicationId = app.cloud.applicationId || app.appId; + app.cloud.javascriptKey = app.cloud.javascriptKey || app.javascriptKey; + app.cloud.masterKey = app.cloud.masterKey || app.masterKey; + app.cloud.serverURL = app.cloud.serverURL || app.serverURL; + } + + // Global configuration + app.databaseAdapter = app.databaseAdapter || args.databaseAdapter; + app.filesAdapter = app.filesAdapter || args.filesAdapter; + app.jsonCacheDir = app.jsonCacheDir || args.jsonCacheDir; + loadConfiguration(app); + }); + return; + } + if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } @@ -65,7 +152,8 @@ function ParseServer(args) { // Make files adapter let filesAdapter = args.filesAdapter || new GridStoreAdapter(); - + let filesController = new FilesController(filesAdapter); + // Make push adapter let pushConfig = args.push; let pushAdapter; @@ -74,6 +162,8 @@ function ParseServer(args) { } else if (pushConfig) { pushAdapter = new ParsePushAdapter(pushConfig) } + + let pushController = new PushController(pushAdapter); // Make logger adapter let loggerAdapter = args.loggerAdapter || new FileLoggerAdapter(); @@ -81,20 +171,23 @@ function ParseServer(args) { if (args.databaseURI) { DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); } + + JSONStorageProvider.setAdapter(new JSONStorageController(args.jsonCacheDir || "./.cache")); + if (args.cloud) { - addParseCloud(); - if (typeof args.cloud === 'function') { + if (typeof args.cloud === 'object') { + CloudCodeLauncher(args.cloud); + } else if (typeof args.cloud === 'function') { + addParseCloud(); args.cloud(Parse) } else if (typeof args.cloud === 'string') { + addParseCloud(); require(args.cloud); } else { - throw "argument 'cloud' must either be a string or a function"; + throw "argument 'cloud' must either be a string or a function or an object"; } - } - let filesController = new FilesController(filesAdapter); - cache.apps[args.appId] = { masterKey: args.masterKey, collectionPrefix: args.collectionPrefix || '', @@ -105,6 +198,7 @@ function ParseServer(args) { fileKey: args.fileKey || 'invalid-file-key', facebookAppIds: args.facebookAppIds || [], filesController: filesController, + pushController: pushController, enableAnonymousUsers: args.enableAnonymousUsers || true, oauth: args.oauth || {}, }; @@ -113,89 +207,37 @@ function ParseServer(args) { if (process.env.FACEBOOK_APP_ID) { cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } - - // Initialize the node client SDK automatically - Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); - if(args.serverURL) { - Parse.serverURL = args.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(); - - // File handling needs to be before default middlewares are applied - api.use('/', filesController.getExpressRouter()); - - // TODO: separate this from the regular ParseServer object - if (process.env.TESTING == 1) { - console.log('enabling integration testing-routes'); - api.use('/', require('./testing-routes').router); - } - - api.use(bodyParser.json({ 'type': '*/*' })); - api.use(middlewares.allowCrossDomain); - api.use(middlewares.allowMethodOverride); - api.use(middlewares.handleParseHeaders); - - let routers = [ - new ClassesRouter().getExpressRouter(), - new UsersRouter().getExpressRouter(), - new SessionsRouter().getExpressRouter(), - new RolesRouter().getExpressRouter(), - require('./analytics'), - new InstallationsRouter().getExpressRouter(), - require('./functions'), - require('./schemas'), - new PushController(pushAdapter).getExpressRouter(), - new LoggerController(loggerAdapter).getExpressRouter() - ]; - if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { - routers.push(require('./global_config')); - } - - let appRouter = new PromiseRouter(); - routers.forEach((router) => { - appRouter.merge(router); - }); - batch.mountOnto(appRouter); - - appRouter.mountOnto(api); - - api.use(middlewares.handleParseErrors); - - return api; + + require("./hooks").load(args.appId); } function addParseCloud() { - Parse.Cloud.Functions = {}; - Parse.Cloud.Validators = {}; - Parse.Cloud.Triggers = { - beforeSave: {}, - beforeDelete: {}, - afterSave: {}, - afterDelete: {} - }; Parse.Cloud.define = function(functionName, handler, validationHandler) { - Parse.Cloud.Functions[functionName] = handler; - Parse.Cloud.Validators[functionName] = validationHandler; + triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); }; Parse.Cloud.beforeSave = function(parseClass, handler) { var className = getClassName(parseClass); - Parse.Cloud.Triggers.beforeSave[className] = handler; + triggers.addTrigger('beforeSave', className, handler, Parse.applicationId); }; Parse.Cloud.beforeDelete = function(parseClass, handler) { var className = getClassName(parseClass); - Parse.Cloud.Triggers.beforeDelete[className] = handler; + triggers.addTrigger('beforeDelete', className, handler, Parse.applicationId); }; Parse.Cloud.afterSave = function(parseClass, handler) { var className = getClassName(parseClass); - Parse.Cloud.Triggers.afterSave[className] = handler; + triggers.addTrigger('afterSave', className, handler, Parse.applicationId); }; Parse.Cloud.afterDelete = function(parseClass, handler) { var className = getClassName(parseClass); - Parse.Cloud.Triggers.afterDelete[className] = handler; + triggers.addTrigger('afterDelete', className, handler, Parse.applicationId); + }; + if (process.env.NODE_ENV == "test") { + Parse.Hooks = Parse.Hooks || {}; + Parse.Cloud._removeHook = function(category, name, type, applicationId) { + applicationId = applicationId || Parse.applicationId; + triggers._unregister(applicationId, category, name, type); + } }; Parse.Cloud.httpRequest = httpRequest; global.Parse = Parse; diff --git a/src/middlewares.js b/src/middlewares.js index 7dcf8889a5..e7fd6deadb 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -23,7 +23,7 @@ function handleParseHeaders(req, res, next) { clientKey: req.get('X-Parse-Client-Key'), javascriptKey: req.get('X-Parse-Javascript-Key'), dotNetKey: req.get('X-Parse-Windows-Key'), - restAPIKey: req.get('X-Parse-REST-API-Key') + restAPIKey: req.get('X-Parse-REST-API-Key'), }; if (req.body && req.body._noBody) { @@ -89,7 +89,6 @@ function handleParseHeaders(req, res, next) { req.info = info; var isMaster = (info.masterKey === req.config.masterKey); - if (isMaster) { req.auth = new auth.Auth(req.config, true); next(); diff --git a/src/rest.js b/src/rest.js index 552fa6be8c..8c46485e31 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.'); @@ -69,7 +69,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(); }); } @@ -89,8 +89,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 fadb03f085..ea497c0752 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,15 +9,80 @@ 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; }; +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, @@ -49,7 +114,11 @@ 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() { + success: function(response) { + // Use the JSON response + if (response && request.triggerName === Types.beforeSave) { + return resolve(response); + } var response = {}; if (request.triggerName === Types.beforeSave) { response['object'] = request.object.toJSON(); @@ -68,15 +137,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); }); }; @@ -91,10 +164,19 @@ function inflate(data, restObject) { return Parse.Object.fromJSON(copy); } -module.exports = { - getTrigger: getTrigger, - getRequestObject: getRequestObject, - inflate: inflate, - maybeRunTrigger: maybeRunTrigger, - 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; From b64461b7154a82c69a68e0a3235622689ccb2c4e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 18 Feb 2016 00:09:35 -0500 Subject: [PATCH 2/2] Refactors Parse.Cloud.js architecture --- spec/ParseServer-Cloud.spec.js | 137 ++++++++++----------- spec/helper.js | 1 - src/cloud-code/Parse.Cloud.js | 214 ++++++++++++++++++++++----------- src/cloud-code/index.js | 44 ++++--- src/cloud-code/launcher.js | 1 + src/cloud/main-2.js | 15 ++- src/index.js | 58 ++------- 7 files changed, 259 insertions(+), 211 deletions(-) diff --git a/spec/ParseServer-Cloud.spec.js b/spec/ParseServer-Cloud.spec.js index c6806721d4..a9d4251dc1 100644 --- a/spec/ParseServer-Cloud.spec.js +++ b/spec/ParseServer-Cloud.spec.js @@ -3,6 +3,8 @@ var Parse = require("parse/node"); var apps = configuration.applications; var configLoader = require("../bin/config"); var Server = require("../src/cloud-code"); +var ParseCloud = require("../src/cloud-code/Parse.Cloud"); +Parse.Hooks = require("../src/cloud-code/Parse.Hooks"); var jsonCacheDir = "./.cache"; var express = require("express"); var databaseURI = process.env.DATABASE_URI; @@ -12,6 +14,10 @@ var ParseServer = require('../src/index').ParseServer; var port = 8379; var serverURL = 'http://localhost:' + port + '/1'; +for(var i in configuration.applications) { + configuration.applications[i].serverURL = serverURL; +} + var app = express(); var server = app.listen(port); @@ -19,32 +25,28 @@ var server = app.listen(port); var api = new ParseServer(configuration); app.use('/1', api); -function createEchoHook() { - return Parse.Cloud.define("echoParseKeys", (req, res) => { - res.success({ applicationId: Parse.applicationId, - javascriptKey: Parse.javascriptKey, - masterKey: Parse.masterKey }); - }); +function use(app) { + Parse.initialize(app.appId || app.applicationId, app.javascriptKey, app.masterKey); + Parse.serverURL = app.serverURL; } -function createBeforeSaveHook() { - return Parse.Cloud.beforeSave("InjectAppId", (req, res) => { - req.object.set('applicationId', Parse.applicationId); - req.object.set('javascriptKey', Parse.javascriptKey); - req.object.set('masterKey', Parse.masterKey); - res.success(); - }); -} +var shouldWait = process.env.WAIT_FOR_SERVER; describe('Multi Server Testing', () => { beforeEach((done) => { // Set the proper Pare serverURL - Parse.initialize("test2", "test2", "test2"); - Parse.serverURL = serverURL; - done(); + use(apps[0]); + if (shouldWait) { + shouldWait = false; + setTimeout(() => { + done(); + }, 500); + } else { + done(); + } }) it('first app should have hello', done => { - Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); + Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); Parse.Cloud.run('hello', {}, (result, error) => { expect(result).toEqual('Hello world!'); done(); @@ -52,51 +54,54 @@ describe('Multi Server Testing', () => { }); it('second app should have hello', done => { - Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + use(apps[1]); Parse.Cloud.run('hello', {}, (result, error) => { expect(result).toEqual('Hello world'); + console.error(error); done(); }); }); - it('should echo the right applicatio ID', done => { - Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); - createEchoHook(); - Parse.Cloud.run('echoParseKeys', {}, (result, error) => { + it('should echo the right application ID', done => { + var hit = 0; + function doneIfNeeded() { + hit++; + if (hit != 2) { + return; + } + done(); + } + use(apps[1]); + Parse.Cloud.run('echoParseKeys', {}).then((result) => { expect(result.applicationId).toEqual(apps[1].appId); expect(result.javascriptKey).toEqual(apps[1].javascriptKey); expect(result.masterKey).toEqual(apps[1].masterKey); - Parse.Cloud._removeHook("Functions", 'echoParseKeys', null, apps[1].appId); - done(); + use(apps[1]); + doneIfNeeded(); + }, (error) => { + console.error(error); + fail(JSON.stringify(error)); + doneIfNeeded(); }); - Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); - createEchoHook(); - Parse.Cloud.run('echoParseKeys', {}, (result, error) => { - expect(result.applicationId).toEqual(apps[0].appId); - expect(result.javascriptKey).toEqual(apps[0].javascriptKey); - expect(result.masterKey).toEqual(apps[0].masterKey); - Parse.Cloud._removeHook("Functions", 'echoParseKeys', null, apps[0].appId); - done(); + use(apps[0]); + Parse.Cloud.run('echoParseKeys', {}).then((result) => { + fail("This function should not be defined"); + doneIfNeeded(); + }, (error) => { + + doneIfNeeded(); }); }); it('should delete the proper hook and not leak', done => { - Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); - createEchoHook(); + use(apps[1]); Parse.Cloud.run('echoParseKeys', {}).then( (result) => { expect(result.applicationId).toEqual(apps[1].appId); expect(result.javascriptKey).toEqual(apps[1].javascriptKey); expect(result.masterKey).toEqual(apps[1].masterKey); - Parse.Cloud._removeHook("Functions", 'echoParseKeys'); - return Parse.Promise.as(); - }).then( () => { - Parse.initialize(apps[0].appId, apps[0].javascriptKey, apps[0].masterKey); - return Parse.Cloud.run('echoParseKeys', {}); - }).then( (res) => { - fail("this call should not succeed"); done(); }).fail( (err) => { expect(err.code).toEqual(141); @@ -107,25 +112,23 @@ describe('Multi Server Testing', () => { it('should create the proper beforeSave and set the proper app ID', done => { - Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); - createBeforeSaveHook(); + use(apps[1]); var obj = new Parse.Object('InjectAppId'); - obj.save().then( () => { + return obj.save().then( () => { var query = new Parse.Query('InjectAppId'); query.get(obj.id).then( (objAgain) => { expect(objAgain.get('applicationId')).toEqual(apps[1].appId); expect(objAgain.get('javascriptKey')).toEqual(apps[1].javascriptKey); expect(objAgain.get('masterKey')).toEqual(apps[1].masterKey); - Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); done(); }, (error) => { - fail(error); - Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); + fail("Failed getting object"); + fail(JSON.stringify(error)); done(); }); }, (error) => { - fail(error); - Parse.Cloud._removeHook("Triggers", 'beforeSave', 'InjectAppId'); + fail("Failed saving obj"); + fail(JSON.stringify(error)); done(); }); @@ -133,7 +136,7 @@ describe('Multi Server Testing', () => { it('should create an object in the proper DB (and not the other)', done => { - Parse.initialize(apps[1].appId, apps[1].javascriptKey, apps[1].masterKey); + use(apps[1]); var obj = new Parse.Object('SomeObject'); obj.save().then( () => { var query = new Parse.Query('SomeObject'); @@ -171,14 +174,14 @@ describe('Multi Server Testing', () => { applicationId: apps[1].appId, javascriptKey: apps[1].javascriptKey, masterKey: apps[1].masterKey, - port: 12345, + port: 12355, main: "../cloud/main-2.js", - serverURL: Parse.serverURL, + serverURL: serverURL, hooksCreationStrategy: "always" }; + var server = new Server(config); - Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - Parse.serverURL = config.serverURL; + Parse.Cloud.define("myCloud", (req, res) => { res.success("code!"); }).then( () => { @@ -205,16 +208,14 @@ describe('Multi Server Testing', () => { applicationId: apps[1].appId, javascriptKey: apps[1].javascriptKey, masterKey: apps[1].masterKey, - port: 12345, + port: 12346, main: "../cloud/main.js", - serverURL: Parse.serverURL, + serverURL: serverURL, hooksCreationStrategy: "always" }; var server = new Server(config); var triggerTime = 0; - Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - Parse.serverURL = config.serverURL; // Register a mock beforeSave hook Parse.Cloud.beforeSave('GameScore', (req, res) => { var object = req.object; @@ -248,9 +249,7 @@ describe('Multi Server Testing', () => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); // Clear mock beforeSave - if (Parse.Cloud._removeHook) { - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - }; + Parse.Hooks.deleteTrigger('GameScore', 'beforeSave'); server.close(); done(); }, (error) => { @@ -259,7 +258,7 @@ describe('Multi Server Testing', () => { done(); }); }, (err) => { - fail(err); + fail(JSON.strngify(err)); server.close(); done(); }); @@ -273,15 +272,13 @@ describe('Multi Server Testing', () => { applicationId: apps[1].appId, javascriptKey: apps[1].javascriptKey, masterKey: apps[1].masterKey, - port: 12345, + port: 12347, main: "../cloud/main.js", - serverURL: Parse.serverURL, + serverURL: serverURL, hooksCreationStrategy: "always" }; var server = new Server(config); - Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - Parse.serverURL = config.serverURL; - + Parse.Cloud.define("hello_world", (req, res) => { fail("This shoud not be called!"); @@ -293,6 +290,10 @@ describe('Multi Server Testing', () => { expect(res).toBeUndefined(); return Parse.Cloud.run("hello_world", {}); + }, function(err){ + fail(err); + server.close(); + done(); }).then( (res) => { expect(res).toBeUndefined(); diff --git a/spec/helper.js b/spec/helper.js index a9ef869977..fee57d9fa3 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -47,7 +47,6 @@ Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { Parse.initialize('test', 'test', 'test'); Parse.serverURL = serverURL; - mockFacebook(); Parse.User.enableUnsafeCurrentUser(); done(); }); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index dd7ff3fa52..587526b42d 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,10 +1,51 @@ +var Parse = require("parse/node"); var ParseCloudExpress = require("parse-cloud-express"); -var Parse = ParseCloudExpress.Parse; -Parse.Hooks = require("./Parse.Hooks") +Parse.Hooks = require("./Parse.Hooks"); +var triggers = require("../triggers"); + +// The In memory ParseCloud + +function getClassName(parseClass) { + if (parseClass && parseClass.className) { + return parseClass.className; + } + return 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); +}; + +Parse.Cloud._removeHook = function(category, name, type, applicationId) { + applicationId = applicationId || Parse.applicationId; + triggers._unregister(applicationId, category, name, type); +}; // Store the original Parse Cloud instance // to prevent multiple wrapping const PARSE_CLOUD_OVERRIDES = ["define", "beforeSave", "afterSave", "beforeDelete", "afterDelete"]; + const PARSE_CLOUD_FUNCTIONS = PARSE_CLOUD_OVERRIDES.reduce(function(a, b){ a[b] = ParseCloudExpress.Parse.Cloud[b]; return a; @@ -16,61 +57,54 @@ var hooksCreationStrategy = { 'try': 'try', // try to create a hook, but don't override if exists }; -Parse.Cloud.hooksCreationStrategy = hooksCreationStrategy; +function buildURL(name, trigger, config) { + trigger = trigger || "function"; + var URL = config.mountPath+"/"+trigger+"_"+name; + return URL; +} -module.exports = ParseCloudExpress.Parse.Cloud; -module.exports.injectAutoRegistration = function(config) { +function registerHook(type, name, trigger, cloudServerURL, creationStrategy, config) { - Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - Parse.serverURL = config.serverURL; - - var buildURL = function(name, trigger) { - trigger = trigger || "function"; - var URL = config.mountPath+"/"+trigger+"_"+name; - return URL; - } + var url = ""; + var hookURL; + var data = {}; - var registerHook = function(type, name, trigger, cloudServerURL, creationStrategy) { - - var url = ""; - var hookURL; - var data = {}; - - if (type === "function") { - url = "/hooks/functions"; - data.functionName = name; - hookURL = buildURL(name); - creationStrategy = cloudServerURL; - cloudServerURL = trigger; - } else if (type == "trigger") { - url = "/hooks/triggers"; - data.className = name; - data.triggerName = trigger; - hookURL = buildURL(name, trigger); - } - - // No creation strategy, do nothing - if (!creationStrategy || creationStrategy == hooksCreationStrategy.never) { - return Parse.Promise.as(); - } + if (type === "function") { + url = "/hooks/functions"; + data.functionName = name; + hookURL = buildURL(name, "function", config); - data.url = cloudServerURL + hookURL; - return Parse.Hooks.create(data).fail(function(err){ - if (creationStrategy == hooksCreationStrategy.always) { - return Parse.Hooks.update(data); - } - // Ignore the error then - return Parse.Promise.as(err); - }); + } else if (type == "trigger") { + url = "/hooks/triggers"; + data.className = name; + data.triggerName = trigger; + hookURL = buildURL(name, trigger, config); + } + + // No creation strategy, do nothing + if (!creationStrategy || creationStrategy == hooksCreationStrategy.never) { + return Parse.Promise.as(); } - var wrapHandler = function(handler) { - return function(request, response) { - var _success = response.success; - + data.url = cloudServerURL + hookURL; + + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + return Parse.Hooks.create(data).fail(function(err){ + if (creationStrategy == hooksCreationStrategy.always) { Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - Parse.serverURL = config.serverURL; + Parse.serverURL = config.serverURL; + return Parse.Hooks.update(data); + } + // Ignore the error then + return Parse.Promise.as(err); + }); +} +function wrapHandler(handler, config) { + return (request, response) => { + const _success = response.success; + response.success = function(args) { var responseValue = args; if (request.object) { @@ -81,30 +115,64 @@ module.exports.injectAutoRegistration = function(config) { } _success(responseValue); } - + + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; return handler(request, response); }; - }; - - var ParseCloudOverrides = PARSE_CLOUD_OVERRIDES.reduce(function(cloud, triggerName){ - var currentTrigger = PARSE_CLOUD_FUNCTIONS[triggerName]; - cloud[triggerName] = function(name, handler, creationStrategy) { - creationStrategy = creationStrategy || config.hooksCreationStrategy; - var promise; - if (triggerName === "define") { - promise = registerHook("function", name, config.cloudServerURL, creationStrategy); - } else { - promise = registerHook("trigger", name, triggerName, config.cloudServerURL, creationStrategy); - } - if (triggerName == "beforeSave") { - handler = wrapHandler(handler); - }; - currentTrigger(name, handler); - return promise; - } - return cloud; - }, {}); - // mount the overrides on the ParseCloudExpress.Parse.Cloud - Object.assign(ParseCloudExpress.Parse.Cloud, ParseCloudOverrides); - Parse.Cloud = ParseCloudExpress.Parse.Cloud; +}; + +var CONFIGURATIONS = {}; + +Parse.Cloud.hooksCreationStrategy = hooksCreationStrategy; + +PARSE_CLOUD_OVERRIDES.map(function(triggerName){ + Object.defineProperty(Parse.Cloud, triggerName, { + get() { + return function(name, handler, creationStrategy) { + const config = CONFIGURATIONS[Parse.applicationId]; + if (!config) { + ParseCloud[triggerName](name, handler, creationStrategy); + return Parse.Promise.as(); + } + const cloudServerURL = Parse.Cloud.serverURL; + + config.mountPath = config.mountPath || "/_hooks"; + creationStrategy = creationStrategy || config.hooksCreationStrategy; + var promise; + if (triggerName === "define") { + promise = registerHook("function", name, null, cloudServerURL, creationStrategy, config); + } else { + name = getClassName(name); + promise = registerHook("trigger", name, triggerName, cloudServerURL, creationStrategy, config); + } + + Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); + Parse.serverURL = config.serverURL; + handler = wrapHandler(handler, config); + PARSE_CLOUD_FUNCTIONS[triggerName](name, handler); + return promise; + } + } + }); +}); + +Object.defineProperty(Parse.Cloud, "serverURL", { + get() { + const config = CONFIGURATIONS[Parse.applicationId]; + if (config) { + return config.cloudServerURL || `http://localhost:${config.port}`; + } + return; + } +}) + +Parse.Cloud.registerConfiguration = function(config) { + CONFIGURATIONS[config.applicationId] = config; } + +Parse.Cloud.unregisterApplicationId = function(applicationId) { + delete CONFIGURATIONS[applicationId]; +} + +module.exports = Parse.Cloud; diff --git a/src/cloud-code/index.js b/src/cloud-code/index.js index 3689f8d228..1fc3c396e7 100644 --- a/src/cloud-code/index.js +++ b/src/cloud-code/index.js @@ -1,37 +1,41 @@ /*jshint node:true */ +var ParseCloudExpressApp = require('parse-cloud-express').app; +var express = require("express"); +var bodyParser = require('body-parser'); var CloudCodeServer = function(config) { 'use strict'; - var path = require("path"); - config.cloudServerURL = config.cloudServerURL || `http://localhost:${config.port}`; - config.mountPath = config.mountPath || "/_hooks"; + var Parse = require("parse/node"); - global.Parse = Parse; - Parse.initialize(config.applicationId, config.javascriptKey, config.masterKey); - var ParseCloudExpress = require('parse-cloud-express'); + config.cloudServerURL = config.cloudServerURL || `http://localhost:${config.port}`; + config.mountPath = config.mountPath || "/_hooks"; + + global.Parse = require("parse/node"); + + // Mount Parse.Cloud require("./Parse.Cloud"); - Parse.Cloud.injectAutoRegistration(config); - - var express = require("express"); - var bodyParser = require('body-parser'); - var app = require("express/lib/application"); - + // Register the current configuration + Parse.Cloud.registerConfiguration(config); + + // Setup the Parse app + Parse.applicationId = config.applicationId; + Parse.javascriptKey = config.javascriptKey; + Parse.masterKey = config.masterKey; - var cloudCodeHooksApp = express(); + const cloudCodeHooksApp = express(); cloudCodeHooksApp.use(bodyParser.json({ 'type': '*/*' })); - this.httpServer = cloudCodeHooksApp.listen(config.port); - if (process.env.NODE_ENV !== "test") { - console.log("[%s] Running Cloud Code for "+Parse.applicationId+" on http://localhost:%s", process.pid, config.port); - } + this.httpServer = cloudCodeHooksApp.listen(config.port); - Parse.Cloud.serverURL = config.cloudServerURL; - Parse.Cloud.app = cloudCodeHooksApp; - cloudCodeHooksApp.use(config.mountPath, ParseCloudExpress.app); + cloudCodeHooksApp.use(config.mountPath, ParseCloudExpressApp); this.app = cloudCodeHooksApp; require(config.main); + + if (process.env.NODE_ENV !== "test") { + console.log("[%s] Running Cloud Code for "+Parse.applicationId+" on http://localhost:%s", process.pid, config.port); + } } CloudCodeServer.prototype.close = function() { this.httpServer.close(); diff --git a/src/cloud-code/launcher.js b/src/cloud-code/launcher.js index f8979e0aa7..72ab7e590f 100644 --- a/src/cloud-code/launcher.js +++ b/src/cloud-code/launcher.js @@ -25,4 +25,5 @@ module.exports = function(options) { }); cloudCode.start(); + return cloudCode; } \ No newline at end of file diff --git a/src/cloud/main-2.js b/src/cloud/main-2.js index 682202465c..57386853ac 100644 --- a/src/cloud/main-2.js +++ b/src/cloud/main-2.js @@ -1,3 +1,16 @@ Parse.Cloud.define('hello', function(req, res) { res.success('Hello world'); -}); \ No newline at end of file +}); + +Parse.Cloud.beforeSave("InjectAppId", (req, res) => { + req.object.set('applicationId', Parse.applicationId); + req.object.set('javascriptKey', Parse.javascriptKey); + req.object.set('masterKey', Parse.masterKey); + res.success(); +}); + +Parse.Cloud.define("echoParseKeys", (req, res) => { + res.success({ applicationId: Parse.applicationId, + javascriptKey: Parse.javascriptKey, + masterKey: Parse.masterKey }); +}); diff --git a/src/index.js b/src/index.js index 81f8769fb9..02b59c0dfd 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,6 @@ var batch = require('./batch'), multer = require('multer'), Parse = require('parse/node').Parse, PromiseRouter = require('./PromiseRouter'), - httpRequest = require("parse-cloud-express/lib/httpRequest"), triggers = require('./triggers'), hooks = require('./hooks'), path = require("path"), @@ -32,9 +31,9 @@ import { RolesRouter } from './Routers/RolesRouter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; -// Mutate the Parse object to add the Cloud Code handlers -addParseCloud(); - +// Load Parse and mutate Parse.Cloud +global.Parse = Parse; +require("./cloud-code/Parse.Cloud"); // ParseServer works like a constructor of an express app. // The args that we understand are: // "databaseAdapter": a class like ExportAdapter providing create, find, @@ -60,7 +59,7 @@ addParseCloud(); // "push": optional key from configure push function ParseServer(args) { - + loadConfiguration(args); // This app serves the Parse API directly. @@ -145,7 +144,9 @@ function loadConfiguration(args) { if (!args.appId || !args.masterKey) { throw 'You must provide an appId and masterKey!'; } - + Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey); + Parse.serverURL = args.serverURL; + if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); } @@ -176,12 +177,12 @@ function loadConfiguration(args) { if (args.cloud) { if (typeof args.cloud === 'object') { + // Register configuration for cloud code + Parse.Cloud.registerConfiguration(args.cloud); CloudCodeLauncher(args.cloud); } else if (typeof args.cloud === 'function') { - addParseCloud(); args.cloud(Parse) } else if (typeof args.cloud === 'string') { - addParseCloud(); require(args.cloud); } else { throw "argument 'cloud' must either be a string or a function or an object"; @@ -207,47 +208,8 @@ function loadConfiguration(args) { if (process.env.FACEBOOK_APP_ID) { cache.apps[args.appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } - - require("./hooks").load(args.appId); -} - -function addParseCloud() { - Parse.Cloud.define = function(functionName, handler, validationHandler) { - triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); - }; - Parse.Cloud.beforeSave = function(parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger('beforeSave', className, handler, Parse.applicationId); - }; - Parse.Cloud.beforeDelete = function(parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger('beforeDelete', className, handler, Parse.applicationId); - }; - Parse.Cloud.afterSave = function(parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger('afterSave', className, handler, Parse.applicationId); - }; - Parse.Cloud.afterDelete = function(parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger('afterDelete', className, handler, Parse.applicationId); - }; - if (process.env.NODE_ENV == "test") { - Parse.Hooks = Parse.Hooks || {}; - Parse.Cloud._removeHook = function(category, name, type, applicationId) { - applicationId = applicationId || Parse.applicationId; - triggers._unregister(applicationId, category, name, type); - } - }; - Parse.Cloud.httpRequest = httpRequest; - global.Parse = Parse; -} - -function getClassName(parseClass) { - if (parseClass && parseClass.className) { - return parseClass.className; - } - return parseClass; + require("./hooks").load(args.appId); } module.exports = {