|
| 1 | +// ParseServer - open-source compatible API Server for Parse apps |
| 2 | + |
| 3 | +import 'babel-polyfill'; |
| 4 | + |
| 5 | +var batch = require('./batch'), |
| 6 | + bodyParser = require('body-parser'), |
| 7 | + DatabaseAdapter = require('./DatabaseAdapter'), |
| 8 | + express = require('express'), |
| 9 | + middlewares = require('./middlewares'), |
| 10 | + multer = require('multer'), |
| 11 | + Parse = require('parse/node').Parse, |
| 12 | + authDataManager = require('./authDataManager'); |
| 13 | + |
| 14 | +//import passwordReset from './passwordReset'; |
| 15 | +import cache from './cache'; |
| 16 | +import Config from './Config'; |
| 17 | +import parseServerPackage from '../package.json'; |
| 18 | +import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; |
| 19 | +import PromiseRouter from './PromiseRouter'; |
| 20 | +import requiredParameter from './requiredParameter'; |
| 21 | +import { AnalyticsRouter } from './Routers/AnalyticsRouter'; |
| 22 | +import { ClassesRouter } from './Routers/ClassesRouter'; |
| 23 | +import { FeaturesRouter } from './Routers/FeaturesRouter'; |
| 24 | +import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; |
| 25 | +import { FilesController } from './Controllers/FilesController'; |
| 26 | +import { FilesRouter } from './Routers/FilesRouter'; |
| 27 | +import { FunctionsRouter } from './Routers/FunctionsRouter'; |
| 28 | +import { GCSAdapter } from './Adapters/Files/GCSAdapter'; |
| 29 | +import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; |
| 30 | +import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; |
| 31 | +import { HooksController } from './Controllers/HooksController'; |
| 32 | +import { HooksRouter } from './Routers/HooksRouter'; |
| 33 | +import { IAPValidationRouter } from './Routers/IAPValidationRouter'; |
| 34 | +import { InstallationsRouter } from './Routers/InstallationsRouter'; |
| 35 | +import { loadAdapter } from './Adapters/AdapterLoader'; |
| 36 | +import { LiveQueryController } from './Controllers/LiveQueryController'; |
| 37 | +import { LoggerController } from './Controllers/LoggerController'; |
| 38 | +import { LogsRouter } from './Routers/LogsRouter'; |
| 39 | +import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; |
| 40 | +import { PublicAPIRouter } from './Routers/PublicAPIRouter'; |
| 41 | +import { PushController } from './Controllers/PushController'; |
| 42 | +import { PushRouter } from './Routers/PushRouter'; |
| 43 | +import { randomString } from './cryptoUtils'; |
| 44 | +import { RolesRouter } from './Routers/RolesRouter'; |
| 45 | +import { S3Adapter } from './Adapters/Files/S3Adapter'; |
| 46 | +import { SchemasRouter } from './Routers/SchemasRouter'; |
| 47 | +import { SessionsRouter } from './Routers/SessionsRouter'; |
| 48 | +import { setFeature } from './features'; |
| 49 | +import { UserController } from './Controllers/UserController'; |
| 50 | +import { UsersRouter } from './Routers/UsersRouter'; |
| 51 | +import { FileSystemAdapter } from './Adapters/Files/FileSystemAdapter'; |
| 52 | + |
| 53 | +// Mutate the Parse object to add the Cloud Code handlers |
| 54 | +addParseCloud(); |
| 55 | + |
| 56 | +// ParseServer works like a constructor of an express app. |
| 57 | +// The args that we understand are: |
| 58 | +// "databaseAdapter": a class like DatabaseController providing create, find, |
| 59 | +// update, and delete |
| 60 | +// "filesAdapter": a class like GridStoreAdapter providing create, get, |
| 61 | +// and delete |
| 62 | +// "loggerAdapter": a class like FileLoggerAdapter providing info, error, |
| 63 | +// and query |
| 64 | +// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us |
| 65 | +// what database this Parse API connects to. |
| 66 | +// "cloud": relative location to cloud code to require, or a function |
| 67 | +// that is given an instance of Parse as a parameter. Use this instance of Parse |
| 68 | +// to register your cloud code hooks and functions. |
| 69 | +// "appId": the application id to host |
| 70 | +// "masterKey": the master key for requests to this app |
| 71 | +// "facebookAppIds": an array of valid Facebook Application IDs, required |
| 72 | +// if using Facebook login |
| 73 | +// "collectionPrefix": optional prefix for database collection names |
| 74 | +// "fileKey": optional key from Parse dashboard for supporting older files |
| 75 | +// hosted by Parse |
| 76 | +// "clientKey": optional key from Parse dashboard |
| 77 | +// "dotNetKey": optional key from Parse dashboard |
| 78 | +// "restAPIKey": optional key from Parse dashboard |
| 79 | +// "javascriptKey": optional key from Parse dashboard |
| 80 | +// "push": optional key from configure push |
| 81 | + |
| 82 | +class ParseServer { |
| 83 | + |
| 84 | + constructor({ |
| 85 | + appId = requiredParameter('You must provide an appId!'), |
| 86 | + masterKey = requiredParameter('You must provide a masterKey!'), |
| 87 | + appName, |
| 88 | + databaseAdapter, |
| 89 | + filesAdapter, |
| 90 | + push, |
| 91 | + loggerAdapter, |
| 92 | + databaseURI = DatabaseAdapter.defaultDatabaseURI, |
| 93 | + databaseOptions, |
| 94 | + cloud, |
| 95 | + collectionPrefix = '', |
| 96 | + clientKey, |
| 97 | + javascriptKey, |
| 98 | + dotNetKey, |
| 99 | + restAPIKey, |
| 100 | + fileKey = 'invalid-file-key', |
| 101 | + facebookAppIds = [], |
| 102 | + enableAnonymousUsers = true, |
| 103 | + allowClientClassCreation = true, |
| 104 | + oauth = {}, |
| 105 | + serverURL = requiredParameter('You must provide a serverURL!'), |
| 106 | + maxUploadSize = '20mb', |
| 107 | + verifyUserEmails = false, |
| 108 | + emailAdapter, |
| 109 | + publicServerURL, |
| 110 | + customPages = { |
| 111 | + invalidLink: undefined, |
| 112 | + verifyEmailSuccess: undefined, |
| 113 | + choosePassword: undefined, |
| 114 | + passwordResetSuccess: undefined |
| 115 | + }, |
| 116 | + liveQuery = {} |
| 117 | + }) { |
| 118 | + setFeature('serverVersion', parseServerPackage.version); |
| 119 | + // Initialize the node client SDK automatically |
| 120 | + Parse.initialize(appId, javascriptKey || 'unused', masterKey); |
| 121 | + Parse.serverURL = serverURL; |
| 122 | + |
| 123 | + if (databaseAdapter) { |
| 124 | + DatabaseAdapter.setAdapter(databaseAdapter); |
| 125 | + } |
| 126 | + |
| 127 | + if (databaseOptions) { |
| 128 | + DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions); |
| 129 | + } |
| 130 | + |
| 131 | + if (databaseURI) { |
| 132 | + DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); |
| 133 | + } |
| 134 | + |
| 135 | + if (cloud) { |
| 136 | + addParseCloud(); |
| 137 | + if (typeof cloud === 'function') { |
| 138 | + cloud(Parse) |
| 139 | + } else if (typeof cloud === 'string') { |
| 140 | + require(cloud); |
| 141 | + } else { |
| 142 | + throw "argument 'cloud' must either be a string or a function"; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + |
| 147 | + const filesControllerAdapter = loadAdapter(filesAdapter, () => { |
| 148 | + return new GridStoreAdapter(databaseURI); |
| 149 | + }); |
| 150 | + // Pass the push options too as it works with the default |
| 151 | + const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); |
| 152 | + const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); |
| 153 | + const emailControllerAdapter = loadAdapter(emailAdapter); |
| 154 | + // We pass the options and the base class for the adatper, |
| 155 | + // Note that passing an instance would work too |
| 156 | + const filesController = new FilesController(filesControllerAdapter, appId); |
| 157 | + const pushController = new PushController(pushControllerAdapter, appId); |
| 158 | + const loggerController = new LoggerController(loggerControllerAdapter, appId); |
| 159 | + const hooksController = new HooksController(appId, collectionPrefix); |
| 160 | + const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); |
| 161 | + const liveQueryController = new LiveQueryController(liveQuery); |
| 162 | + |
| 163 | + cache.apps.set(appId, { |
| 164 | + masterKey: masterKey, |
| 165 | + serverURL: serverURL, |
| 166 | + collectionPrefix: collectionPrefix, |
| 167 | + clientKey: clientKey, |
| 168 | + javascriptKey: javascriptKey, |
| 169 | + dotNetKey: dotNetKey, |
| 170 | + restAPIKey: restAPIKey, |
| 171 | + fileKey: fileKey, |
| 172 | + facebookAppIds: facebookAppIds, |
| 173 | + filesController: filesController, |
| 174 | + pushController: pushController, |
| 175 | + loggerController: loggerController, |
| 176 | + hooksController: hooksController, |
| 177 | + userController: userController, |
| 178 | + verifyUserEmails: verifyUserEmails, |
| 179 | + allowClientClassCreation: allowClientClassCreation, |
| 180 | + authDataManager: authDataManager(oauth, enableAnonymousUsers), |
| 181 | + appName: appName, |
| 182 | + publicServerURL: publicServerURL, |
| 183 | + customPages: customPages, |
| 184 | + maxUploadSize: maxUploadSize, |
| 185 | + liveQueryController: liveQueryController |
| 186 | + }); |
| 187 | + |
| 188 | + // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability |
| 189 | + if (process.env.FACEBOOK_APP_ID) { |
| 190 | + cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); |
| 191 | + } |
| 192 | + |
| 193 | + Config.validate(cache.apps.get(appId)); |
| 194 | + this.config = cache.apps.get(appId); |
| 195 | + hooksController.load(); |
| 196 | + } |
| 197 | + |
| 198 | + get app() { |
| 199 | + return ParseServer.app(this.config); |
| 200 | + } |
| 201 | + |
| 202 | + static app({maxUploadSize = '20mb'}) { |
| 203 | + // This app serves the Parse API directly. |
| 204 | + // It's the equivalent of https://api.parse.com/1 in the hosted Parse API. |
| 205 | + var api = express(); |
| 206 | + //api.use("/apps", express.static(__dirname + "/public")); |
| 207 | + // File handling needs to be before default middlewares are applied |
| 208 | + api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({ |
| 209 | + maxUploadSize: maxUploadSize |
| 210 | + })); |
| 211 | + |
| 212 | + api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp()); |
| 213 | + |
| 214 | + // TODO: separate this from the regular ParseServer object |
| 215 | + if (process.env.TESTING == 1) { |
| 216 | + api.use('/', require('./testing-routes').router); |
| 217 | + } |
| 218 | + |
| 219 | + api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize })); |
| 220 | + api.use(middlewares.allowCrossDomain); |
| 221 | + api.use(middlewares.allowMethodOverride); |
| 222 | + api.use(middlewares.handleParseHeaders); |
| 223 | + |
| 224 | + let routers = [ |
| 225 | + new ClassesRouter(), |
| 226 | + new UsersRouter(), |
| 227 | + new SessionsRouter(), |
| 228 | + new RolesRouter(), |
| 229 | + new AnalyticsRouter(), |
| 230 | + new InstallationsRouter(), |
| 231 | + new FunctionsRouter(), |
| 232 | + new SchemasRouter(), |
| 233 | + new PushRouter(), |
| 234 | + new LogsRouter(), |
| 235 | + new IAPValidationRouter(), |
| 236 | + new FeaturesRouter(), |
| 237 | + ]; |
| 238 | + |
| 239 | + if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { |
| 240 | + routers.push(new GlobalConfigRouter()); |
| 241 | + } |
| 242 | + |
| 243 | + if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { |
| 244 | + routers.push(new HooksRouter()); |
| 245 | + } |
| 246 | + |
| 247 | + let routes = routers.reduce((memo, router) => { |
| 248 | + return memo.concat(router.routes); |
| 249 | + }, []); |
| 250 | + |
| 251 | + let appRouter = new PromiseRouter(routes); |
| 252 | + |
| 253 | + batch.mountOnto(appRouter); |
| 254 | + |
| 255 | + api.use(appRouter.expressApp()); |
| 256 | + |
| 257 | + api.use(middlewares.handleParseErrors); |
| 258 | + |
| 259 | + //This causes tests to spew some useless warnings, so disable in test |
| 260 | + if (!process.env.TESTING) { |
| 261 | + process.on('uncaughtException', (err) => { |
| 262 | + if ( err.code === "EADDRINUSE" ) { // user-friendly message for this common error |
| 263 | + console.log(`Unable to listen on port ${err.port}. The port is already in use.`); |
| 264 | + process.exit(0); |
| 265 | + } else { |
| 266 | + throw err; |
| 267 | + } |
| 268 | + }); |
| 269 | + } |
| 270 | + return api; |
| 271 | + } |
| 272 | + |
| 273 | + static createLiveQueryServer(httpServer, config) { |
| 274 | + return new ParseLiveQueryServer(httpServer, config); |
| 275 | + } |
| 276 | +} |
| 277 | + |
| 278 | +function addParseCloud() { |
| 279 | + const ParseCloud = require("./cloud-code/Parse.Cloud"); |
| 280 | + Object.assign(Parse.Cloud, ParseCloud); |
| 281 | + global.Parse = Parse; |
| 282 | +} |
| 283 | + |
| 284 | +export default ParseServer; |
0 commit comments