diff --git a/src/ParseServer.js b/src/ParseServer.js new file mode 100644 index 00000000..7d19407f --- /dev/null +++ b/src/ParseServer.js @@ -0,0 +1,284 @@ +// ParseServer - open-source compatible API Server for Parse apps + +import 'babel-polyfill'; + +var batch = require('./batch'), + bodyParser = require('body-parser'), + DatabaseAdapter = require('./DatabaseAdapter'), + express = require('express'), + middlewares = require('./middlewares'), + multer = require('multer'), + Parse = require('parse/node').Parse, + authDataManager = require('./authDataManager'); + +//import passwordReset from './passwordReset'; +import cache from './cache'; +import Config from './Config'; +import parseServerPackage from '../package.json'; +import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; +import PromiseRouter from './PromiseRouter'; +import requiredParameter from './requiredParameter'; +import { AnalyticsRouter } from './Routers/AnalyticsRouter'; +import { ClassesRouter } from './Routers/ClassesRouter'; +import { FeaturesRouter } from './Routers/FeaturesRouter'; +import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; +import { FilesController } from './Controllers/FilesController'; +import { FilesRouter } from './Routers/FilesRouter'; +import { FunctionsRouter } from './Routers/FunctionsRouter'; +import { GCSAdapter } from './Adapters/Files/GCSAdapter'; +import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; +import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; +import { HooksController } from './Controllers/HooksController'; +import { HooksRouter } from './Routers/HooksRouter'; +import { IAPValidationRouter } from './Routers/IAPValidationRouter'; +import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { loadAdapter } from './Adapters/AdapterLoader'; +import { LiveQueryController } from './Controllers/LiveQueryController'; +import { LoggerController } from './Controllers/LoggerController'; +import { LogsRouter } from './Routers/LogsRouter'; +import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; +import { PublicAPIRouter } from './Routers/PublicAPIRouter'; +import { PushController } from './Controllers/PushController'; +import { PushRouter } from './Routers/PushRouter'; +import { randomString } from './cryptoUtils'; +import { RolesRouter } from './Routers/RolesRouter'; +import { S3Adapter } from './Adapters/Files/S3Adapter'; +import { SchemasRouter } from './Routers/SchemasRouter'; +import { SessionsRouter } from './Routers/SessionsRouter'; +import { setFeature } from './features'; +import { UserController } from './Controllers/UserController'; +import { UsersRouter } from './Routers/UsersRouter'; +import { FileSystemAdapter } from './Adapters/Files/FileSystemAdapter'; + +// Mutate the Parse object to add the Cloud Code handlers +addParseCloud(); + +// ParseServer works like a constructor of an express app. +// The args that we understand are: +// "databaseAdapter": a class like DatabaseController providing create, find, +// update, and delete +// "filesAdapter": a class like GridStoreAdapter providing create, get, +// and delete +// "loggerAdapter": a class like FileLoggerAdapter providing info, error, +// and query +// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us +// what database this Parse API connects to. +// "cloud": relative location to cloud code to require, or a function +// that is given an instance of Parse as a parameter. Use this instance of Parse +// to register your cloud code hooks and functions. +// "appId": the application id to host +// "masterKey": the master key for requests to this app +// "facebookAppIds": an array of valid Facebook Application IDs, required +// if using Facebook login +// "collectionPrefix": optional prefix for database collection names +// "fileKey": optional key from Parse dashboard for supporting older files +// hosted by Parse +// "clientKey": optional key from Parse dashboard +// "dotNetKey": optional key from Parse dashboard +// "restAPIKey": optional key from Parse dashboard +// "javascriptKey": optional key from Parse dashboard +// "push": optional key from configure push + +class ParseServer { + + constructor({ + appId = requiredParameter('You must provide an appId!'), + masterKey = requiredParameter('You must provide a masterKey!'), + appName, + databaseAdapter, + filesAdapter, + push, + loggerAdapter, + databaseURI = DatabaseAdapter.defaultDatabaseURI, + databaseOptions, + cloud, + collectionPrefix = '', + clientKey, + javascriptKey, + dotNetKey, + restAPIKey, + fileKey = 'invalid-file-key', + facebookAppIds = [], + enableAnonymousUsers = true, + allowClientClassCreation = true, + oauth = {}, + serverURL = requiredParameter('You must provide a serverURL!'), + maxUploadSize = '20mb', + verifyUserEmails = false, + emailAdapter, + publicServerURL, + customPages = { + invalidLink: undefined, + verifyEmailSuccess: undefined, + choosePassword: undefined, + passwordResetSuccess: undefined + }, + liveQuery = {} + }) { + setFeature('serverVersion', parseServerPackage.version); + // Initialize the node client SDK automatically + Parse.initialize(appId, javascriptKey || 'unused', masterKey); + Parse.serverURL = serverURL; + + if (databaseAdapter) { + DatabaseAdapter.setAdapter(databaseAdapter); + } + + if (databaseOptions) { + DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions); + } + + if (databaseURI) { + DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); + } + + if (cloud) { + addParseCloud(); + if (typeof cloud === 'function') { + cloud(Parse) + } else if (typeof cloud === 'string') { + require(cloud); + } else { + throw "argument 'cloud' must either be a string or a function"; + } + } + + + const filesControllerAdapter = loadAdapter(filesAdapter, () => { + return new GridStoreAdapter(databaseURI); + }); + // Pass the push options too as it works with the default + const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); + const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); + const emailControllerAdapter = loadAdapter(emailAdapter); + // We pass the options and the base class for the adatper, + // Note that passing an instance would work too + const filesController = new FilesController(filesControllerAdapter, appId); + const pushController = new PushController(pushControllerAdapter, appId); + const loggerController = new LoggerController(loggerControllerAdapter, appId); + const hooksController = new HooksController(appId, collectionPrefix); + const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); + const liveQueryController = new LiveQueryController(liveQuery); + + cache.apps.set(appId, { + masterKey: masterKey, + serverURL: serverURL, + collectionPrefix: collectionPrefix, + clientKey: clientKey, + javascriptKey: javascriptKey, + dotNetKey: dotNetKey, + restAPIKey: restAPIKey, + fileKey: fileKey, + facebookAppIds: facebookAppIds, + filesController: filesController, + pushController: pushController, + loggerController: loggerController, + hooksController: hooksController, + userController: userController, + verifyUserEmails: verifyUserEmails, + allowClientClassCreation: allowClientClassCreation, + authDataManager: authDataManager(oauth, enableAnonymousUsers), + appName: appName, + publicServerURL: publicServerURL, + customPages: customPages, + maxUploadSize: maxUploadSize, + liveQueryController: liveQueryController + }); + + // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability + if (process.env.FACEBOOK_APP_ID) { + cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); + } + + Config.validate(cache.apps.get(appId)); + this.config = cache.apps.get(appId); + hooksController.load(); + } + + get app() { + return ParseServer.app(this.config); + } + + static app({maxUploadSize = '20mb'}) { + // 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(); + //api.use("/apps", express.static(__dirname + "/public")); + // File handling needs to be before default middlewares are applied + api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({ + maxUploadSize: maxUploadSize + })); + + api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp()); + + // TODO: separate this from the regular ParseServer object + if (process.env.TESTING == 1) { + api.use('/', require('./testing-routes').router); + } + + api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize })); + api.use(middlewares.allowCrossDomain); + api.use(middlewares.allowMethodOverride); + api.use(middlewares.handleParseHeaders); + + let routers = [ + new ClassesRouter(), + new UsersRouter(), + new SessionsRouter(), + new RolesRouter(), + new AnalyticsRouter(), + new InstallationsRouter(), + new FunctionsRouter(), + new SchemasRouter(), + new PushRouter(), + new LogsRouter(), + new IAPValidationRouter(), + new FeaturesRouter(), + ]; + + if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { + routers.push(new GlobalConfigRouter()); + } + + if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { + routers.push(new HooksRouter()); + } + + let routes = routers.reduce((memo, router) => { + return memo.concat(router.routes); + }, []); + + let appRouter = new PromiseRouter(routes); + + batch.mountOnto(appRouter); + + api.use(appRouter.expressApp()); + + api.use(middlewares.handleParseErrors); + + //This causes tests to spew some useless warnings, so disable in test + if (!process.env.TESTING) { + process.on('uncaughtException', (err) => { + if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error + console.log(`Unable to listen on port ${err.port}. The port is already in use.`); + process.exit(0); + } else { + throw err; + } + }); + } + return api; + } + + static createLiveQueryServer(httpServer, config) { + return new ParseLiveQueryServer(httpServer, config); + } +} + +function addParseCloud() { + const ParseCloud = require("./cloud-code/Parse.Cloud"); + Object.assign(Parse.Cloud, ParseCloud); + global.Parse = Parse; +} + +export default ParseServer; diff --git a/src/index.js b/src/index.js index 47563d4b..870f6c5e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,279 +1,7 @@ -// ParseServer - open-source compatible API Server for Parse apps - -import 'babel-polyfill'; - -var batch = require('./batch'), - bodyParser = require('body-parser'), - DatabaseAdapter = require('./DatabaseAdapter'), - express = require('express'), - middlewares = require('./middlewares'), - multer = require('multer'), - Parse = require('parse/node').Parse, - authDataManager = require('./authDataManager'); - -//import passwordReset from './passwordReset'; -import cache from './cache'; -import Config from './Config'; -import parseServerPackage from '../package.json'; -import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; -import PromiseRouter from './PromiseRouter'; -import requiredParameter from './requiredParameter'; -import { AnalyticsRouter } from './Routers/AnalyticsRouter'; -import { ClassesRouter } from './Routers/ClassesRouter'; -import { FeaturesRouter } from './Routers/FeaturesRouter'; -import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; -import { FilesController } from './Controllers/FilesController'; -import { FilesRouter } from './Routers/FilesRouter'; -import { FunctionsRouter } from './Routers/FunctionsRouter'; -import { GCSAdapter } from './Adapters/Files/GCSAdapter'; -import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; -import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; -import { HooksController } from './Controllers/HooksController'; -import { HooksRouter } from './Routers/HooksRouter'; -import { IAPValidationRouter } from './Routers/IAPValidationRouter'; -import { InstallationsRouter } from './Routers/InstallationsRouter'; -import { loadAdapter } from './Adapters/AdapterLoader'; -import { LiveQueryController } from './Controllers/LiveQueryController'; -import { LoggerController } from './Controllers/LoggerController'; -import { LogsRouter } from './Routers/LogsRouter'; -import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; -import { PublicAPIRouter } from './Routers/PublicAPIRouter'; -import { PushController } from './Controllers/PushController'; -import { PushRouter } from './Routers/PushRouter'; -import { randomString } from './cryptoUtils'; -import { RolesRouter } from './Routers/RolesRouter'; -import { S3Adapter } from './Adapters/Files/S3Adapter'; -import { SchemasRouter } from './Routers/SchemasRouter'; -import { SessionsRouter } from './Routers/SessionsRouter'; -import { setFeature } from './features'; -import { UserController } from './Controllers/UserController'; -import { UsersRouter } from './Routers/UsersRouter'; -import { FileSystemAdapter } from './Adapters/Files/FileSystemAdapter'; - -// Mutate the Parse object to add the Cloud Code handlers -addParseCloud(); - -// ParseServer works like a constructor of an express app. -// The args that we understand are: -// "databaseAdapter": a class like DatabaseController providing create, find, -// update, and delete -// "filesAdapter": a class like GridStoreAdapter providing create, get, -// and delete -// "loggerAdapter": a class like FileLoggerAdapter providing info, error, -// and query -// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us -// what database this Parse API connects to. -// "cloud": relative location to cloud code to require, or a function -// that is given an instance of Parse as a parameter. Use this instance of Parse -// to register your cloud code hooks and functions. -// "appId": the application id to host -// "masterKey": the master key for requests to this app -// "facebookAppIds": an array of valid Facebook Application IDs, required -// if using Facebook login -// "collectionPrefix": optional prefix for database collection names -// "fileKey": optional key from Parse dashboard for supporting older files -// hosted by Parse -// "clientKey": optional key from Parse dashboard -// "dotNetKey": optional key from Parse dashboard -// "restAPIKey": optional key from Parse dashboard -// "javascriptKey": optional key from Parse dashboard -// "push": optional key from configure push - -class ParseServer { - - constructor({ - appId = requiredParameter('You must provide an appId!'), - masterKey = requiredParameter('You must provide a masterKey!'), - appName, - databaseAdapter, - filesAdapter, - push, - loggerAdapter, - databaseURI = DatabaseAdapter.defaultDatabaseURI, - databaseOptions, - cloud, - collectionPrefix = '', - clientKey, - javascriptKey, - dotNetKey, - restAPIKey, - fileKey = 'invalid-file-key', - facebookAppIds = [], - enableAnonymousUsers = true, - allowClientClassCreation = true, - oauth = {}, - serverURL = requiredParameter('You must provide a serverURL!'), - maxUploadSize = '20mb', - verifyUserEmails = false, - emailAdapter, - publicServerURL, - customPages = { - invalidLink: undefined, - verifyEmailSuccess: undefined, - choosePassword: undefined, - passwordResetSuccess: undefined - }, - liveQuery = {} - }) { - setFeature('serverVersion', parseServerPackage.version); - // Initialize the node client SDK automatically - Parse.initialize(appId, javascriptKey || 'unused', masterKey); - Parse.serverURL = serverURL; - - if (databaseAdapter) { - DatabaseAdapter.setAdapter(databaseAdapter); - } - - if (databaseOptions) { - DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions); - } - - if (databaseURI) { - DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); - } - - if (cloud) { - addParseCloud(); - if (typeof cloud === 'function') { - cloud(Parse) - } else if (typeof cloud === 'string') { - require(cloud); - } else { - throw "argument 'cloud' must either be a string or a function"; - } - } - - - const filesControllerAdapter = loadAdapter(filesAdapter, () => { - return new GridStoreAdapter(databaseURI); - }); - // Pass the push options too as it works with the default - const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); - const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); - const emailControllerAdapter = loadAdapter(emailAdapter); - // We pass the options and the base class for the adatper, - // Note that passing an instance would work too - const filesController = new FilesController(filesControllerAdapter, appId); - const pushController = new PushController(pushControllerAdapter, appId); - const loggerController = new LoggerController(loggerControllerAdapter, appId); - const hooksController = new HooksController(appId, collectionPrefix); - const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); - const liveQueryController = new LiveQueryController(liveQuery); - - cache.apps.set(appId, { - masterKey: masterKey, - serverURL: serverURL, - collectionPrefix: collectionPrefix, - clientKey: clientKey, - javascriptKey: javascriptKey, - dotNetKey: dotNetKey, - restAPIKey: restAPIKey, - fileKey: fileKey, - facebookAppIds: facebookAppIds, - filesController: filesController, - pushController: pushController, - loggerController: loggerController, - hooksController: hooksController, - userController: userController, - verifyUserEmails: verifyUserEmails, - allowClientClassCreation: allowClientClassCreation, - authDataManager: authDataManager(oauth, enableAnonymousUsers), - appName: appName, - publicServerURL: publicServerURL, - customPages: customPages, - maxUploadSize: maxUploadSize, - liveQueryController: liveQueryController - }); - - // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability - if (process.env.FACEBOOK_APP_ID) { - cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); - } - - Config.validate(cache.apps.get(appId)); - this.config = cache.apps.get(appId); - hooksController.load(); - } - - get app() { - return ParseServer.app(this.config); - } - - static app({maxUploadSize = '20mb'}) { - // 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(); - //api.use("/apps", express.static(__dirname + "/public")); - // File handling needs to be before default middlewares are applied - api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({ - maxUploadSize: maxUploadSize - })); - - api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp()); - - // TODO: separate this from the regular ParseServer object - if (process.env.TESTING == 1) { - api.use('/', require('./testing-routes').router); - } - - api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize })); - api.use(middlewares.allowCrossDomain); - api.use(middlewares.allowMethodOverride); - api.use(middlewares.handleParseHeaders); - - let routers = [ - new ClassesRouter(), - new UsersRouter(), - new SessionsRouter(), - new RolesRouter(), - new AnalyticsRouter(), - new InstallationsRouter(), - new FunctionsRouter(), - new SchemasRouter(), - new PushRouter(), - new LogsRouter(), - new IAPValidationRouter(), - new FeaturesRouter(), - ]; - - if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { - routers.push(new GlobalConfigRouter()); - } - - if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { - routers.push(new HooksRouter()); - } - - let routes = routers.reduce((memo, router) => { - return memo.concat(router.routes); - }, []); - - let appRouter = new PromiseRouter(routes); - - batch.mountOnto(appRouter); - - api.use(appRouter.expressApp()); - - api.use(middlewares.handleParseErrors); - - //This causes tests to spew some useless warnings, so disable in test - if (!process.env.TESTING) { - process.on('uncaughtException', (err) => { - if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error - console.log(`Unable to listen on port ${err.port}. The port is already in use.`); - process.exit(0); - } else { - throw err; - } - }); - } - return api; - } - - static createLiveQueryServer(httpServer, config) { - return new ParseLiveQueryServer(httpServer, config); - } -} +import ParseServer from './ParseServer' +import { GCSAdapter } from './Adapters/Files/GCSAdapter'; +import { S3Adapter } from './Adapters/Files/S3Adapter'; +import { FileSystemAdapter } from './Adapters/Files/FileSystemAdapter'; // Factory function let _ParseServer = function(options) { @@ -283,12 +11,5 @@ let _ParseServer = function(options) { // Mount the create liveQueryServer _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer; -function addParseCloud() { - const ParseCloud = require("./cloud-code/Parse.Cloud"); - Object.assign(Parse.Cloud, ParseCloud); - global.Parse = Parse; -} - export default ParseServer; -export { S3Adapter, GCSAdapter, FileSystemAdapter }; -export { _ParseServer as ParseServer }; +export { S3Adapter, GCSAdapter, FileSystemAdapter, _ParseServer as ParseServer };