Refactors configuration management (#4271)
* Adds flow types / Configuration interfaces * Lets call it options * Use a single interface to generate the configurations * Translates options to definitions only if comments are set * improves logic * Moves objects around * Fixes issue affecting logging of circular objects * fixes undefined env * Moves all defaults to defaults * Adds back CLI defaults * Restored defaults in commander.js * Merge provided defaults and platform defaults * Addresses visual nits * Improves Config.js code * Adds ability to pass the default value in trailing comments * Load platform defaults from the definitions file * proper default values on various options * Adds ParseServer.start and server.start(options) as quick startup methods * Moves creating liveQueryServer http into ParseServer.js * removes dead code * Adds tests to guarantee we can start a LQ Server from main module * Fixes incorrect code regading liveQuery init port * Start a http server for LQ if port is specified * ensure we dont fail if config.port is not set * Specify port * ignore other path skipped in tests * Adds test for custom middleware setting * Refactors new Config into Config.get - Hides AppCache from ParseServer.js, use Config.put which validates * Extracts controller creation into Controllers/index.js - This makes the ParseServer init way simpler * Move serverURL inference into ParseServer * review nits
This commit is contained in:
@@ -5,59 +5,38 @@ var batch = require('./batch'),
|
||||
express = require('express'),
|
||||
middlewares = require('./middlewares'),
|
||||
Parse = require('parse/node').Parse,
|
||||
path = require('path'),
|
||||
url = require('url'),
|
||||
authDataManager = require('./Adapters/Auth');
|
||||
path = require('path');
|
||||
|
||||
import { ParseServerOptions,
|
||||
LiveQueryServerOptions } from './Options';
|
||||
import defaults from './defaults';
|
||||
import * as logging from './logger';
|
||||
import AppCache from './cache';
|
||||
import Config from './Config';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter';
|
||||
import { AnalyticsController } from './Controllers/AnalyticsController';
|
||||
import { CacheController } from './Controllers/CacheController';
|
||||
import { AnalyticsAdapter } from './Adapters/Analytics/AnalyticsAdapter';
|
||||
import { WinstonLoggerAdapter } from './Adapters/Logger/WinstonLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
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 { PushQueue } from './Push/PushQueue';
|
||||
import { PushWorker } from './Push/PushWorker';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { PurgeRouter } from './Routers/PurgeRouter';
|
||||
import { AudiencesRouter } from './Routers/AudiencesRouter';
|
||||
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import SchemaCache from './Controllers/SchemaCache';
|
||||
import ParsePushAdapter from 'parse-server-push-adapter';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from './Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
|
||||
import { ParseServerRESTController } from './ParseServerRESTController';
|
||||
import * as controllers from './Controllers';
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
@@ -90,200 +69,31 @@ addParseCloud();
|
||||
|
||||
class ParseServer {
|
||||
|
||||
constructor({
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
masterKeyIps = [],
|
||||
appName,
|
||||
analyticsAdapter,
|
||||
filesAdapter,
|
||||
push,
|
||||
scheduledPush = false,
|
||||
loggerAdapter,
|
||||
jsonLogs = defaults.jsonLogs,
|
||||
logsFolder = defaults.logsFolder,
|
||||
verbose = defaults.verbose,
|
||||
logLevel = defaults.level,
|
||||
silent = defaults.silent,
|
||||
databaseURI = defaults.DefaultMongoURI,
|
||||
databaseOptions,
|
||||
databaseAdapter,
|
||||
cloud,
|
||||
collectionPrefix = '',
|
||||
clientKey,
|
||||
javascriptKey,
|
||||
dotNetKey,
|
||||
restAPIKey,
|
||||
webhookKey,
|
||||
fileKey,
|
||||
userSensitiveFields = [],
|
||||
enableAnonymousUsers = defaults.enableAnonymousUsers,
|
||||
allowClientClassCreation = defaults.allowClientClassCreation,
|
||||
oauth = {},
|
||||
auth = {},
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
maxUploadSize = defaults.maxUploadSize,
|
||||
verifyUserEmails = defaults.verifyUserEmails,
|
||||
preventLoginWithUnverifiedEmail = defaults.preventLoginWithUnverifiedEmail,
|
||||
emailVerifyTokenValidityDuration,
|
||||
accountLockout,
|
||||
passwordPolicy,
|
||||
cacheAdapter,
|
||||
emailAdapter,
|
||||
publicServerURL,
|
||||
customPages = {
|
||||
invalidLink: undefined,
|
||||
verifyEmailSuccess: undefined,
|
||||
choosePassword: undefined,
|
||||
passwordResetSuccess: undefined
|
||||
},
|
||||
liveQuery = {},
|
||||
sessionLength = defaults.sessionLength, // 1 Year in seconds
|
||||
maxLimit,
|
||||
expireInactiveSessions = defaults.expireInactiveSessions,
|
||||
revokeSessionOnPasswordReset = defaults.revokeSessionOnPasswordReset,
|
||||
schemaCacheTTL = defaults.schemaCacheTTL, // cache for 5s
|
||||
cacheTTL = defaults.cacheTTL, // cache for 5s
|
||||
cacheMaxSize = defaults.cacheMaxSize, // 10000
|
||||
enableSingleSchemaCache = false,
|
||||
objectIdSize = defaults.objectIdSize,
|
||||
__indexBuildCompletionCallbackForTests = () => {},
|
||||
}) {
|
||||
|
||||
constructor(options: ParseServerOptions) {
|
||||
injectDefaults(options);
|
||||
const {
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
cloud,
|
||||
javascriptKey,
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
__indexBuildCompletionCallbackForTests = () => {},
|
||||
} = options;
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
Parse.serverURL = serverURL;
|
||||
if ((databaseOptions || (databaseURI && databaseURI != defaults.DefaultMongoURI) || collectionPrefix !== '') && databaseAdapter) {
|
||||
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.';
|
||||
} else if (!databaseAdapter) {
|
||||
databaseAdapter = this.getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions)
|
||||
} else {
|
||||
databaseAdapter = loadAdapter(databaseAdapter)
|
||||
}
|
||||
|
||||
if (!filesAdapter && !databaseURI) {
|
||||
throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.';
|
||||
}
|
||||
|
||||
userSensitiveFields = Array.from(new Set(userSensitiveFields.concat(
|
||||
defaults.userSensitiveFields,
|
||||
userSensitiveFields
|
||||
)));
|
||||
|
||||
masterKeyIps = Array.from(new Set(masterKeyIps.concat(
|
||||
defaults.masterKeyIps,
|
||||
masterKeyIps
|
||||
)));
|
||||
|
||||
const loggerOptions = { jsonLogs, logsFolder, verbose, logLevel, silent };
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, loggerOptions);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter, appId, loggerOptions);
|
||||
logging.setLogger(loggerController);
|
||||
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridStoreAdapter(databaseURI);
|
||||
});
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
|
||||
const pushOptions = Object.assign({}, push);
|
||||
const pushQueueOptions = pushOptions.queueOptions || {};
|
||||
if (pushOptions.queueOptions) {
|
||||
delete pushOptions.queueOptions;
|
||||
}
|
||||
// Pass the push options too as it works with the default
|
||||
const pushAdapter = loadAdapter(pushOptions && pushOptions.adapter, ParsePushAdapter, pushOptions);
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const pushController = new PushController();
|
||||
const hasPushSupport = !!(pushAdapter && push);
|
||||
const hasPushScheduledSupport = hasPushSupport && (scheduledPush === true);
|
||||
const allControllers = controllers.getControllers(options);
|
||||
|
||||
const {
|
||||
disablePushWorker
|
||||
} = pushQueueOptions;
|
||||
|
||||
const pushControllerQueue = new PushQueue(pushQueueOptions);
|
||||
let pushWorker;
|
||||
if (!disablePushWorker) {
|
||||
pushWorker = new PushWorker(pushAdapter, pushQueueOptions);
|
||||
}
|
||||
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
|
||||
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId, ttl: cacheTTL, maxSize: cacheMaxSize });
|
||||
const cacheController = new CacheController(cacheControllerAdapter, appId);
|
||||
|
||||
const analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter);
|
||||
const analyticsController = new AnalyticsController(analyticsControllerAdapter);
|
||||
|
||||
const liveQueryController = new LiveQueryController(liveQuery);
|
||||
const databaseController = new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache));
|
||||
const hooksController = new HooksController(appId, databaseController, webhookKey);
|
||||
|
||||
const dbInitPromise = databaseController.performInitialization();
|
||||
|
||||
if (Object.keys(oauth).length > 0) {
|
||||
/* eslint-disable no-console */
|
||||
console.warn('oauth option is deprecated and will be removed in a future release, please use auth option instead');
|
||||
if (Object.keys(auth).length > 0) {
|
||||
console.warn('You should use only the auth option.');
|
||||
}
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
auth = Object.assign({}, oauth, auth);
|
||||
|
||||
AppCache.put(appId, {
|
||||
appId,
|
||||
masterKey: masterKey,
|
||||
masterKeyIps:masterKeyIps,
|
||||
serverURL: serverURL,
|
||||
collectionPrefix: collectionPrefix,
|
||||
clientKey: clientKey,
|
||||
javascriptKey: javascriptKey,
|
||||
dotNetKey: dotNetKey,
|
||||
restAPIKey: restAPIKey,
|
||||
webhookKey: webhookKey,
|
||||
fileKey: fileKey,
|
||||
analyticsController: analyticsController,
|
||||
cacheController: cacheController,
|
||||
filesController: filesController,
|
||||
pushController: pushController,
|
||||
loggerController: loggerController,
|
||||
hooksController: hooksController,
|
||||
userController: userController,
|
||||
verifyUserEmails: verifyUserEmails,
|
||||
preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail,
|
||||
emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration,
|
||||
accountLockout: accountLockout,
|
||||
passwordPolicy: passwordPolicy,
|
||||
allowClientClassCreation: allowClientClassCreation,
|
||||
authDataManager: authDataManager(auth, enableAnonymousUsers),
|
||||
appName: appName,
|
||||
publicServerURL: publicServerURL,
|
||||
customPages: customPages,
|
||||
maxUploadSize: maxUploadSize,
|
||||
liveQueryController: liveQueryController,
|
||||
sessionLength: Number(sessionLength),
|
||||
maxLimit: Number(maxLimit),
|
||||
expireInactiveSessions: expireInactiveSessions,
|
||||
jsonLogs,
|
||||
revokeSessionOnPasswordReset,
|
||||
loggerController,
|
||||
databaseController,
|
||||
schemaCacheTTL,
|
||||
enableSingleSchemaCache,
|
||||
userSensitiveFields,
|
||||
pushWorker,
|
||||
pushControllerQueue,
|
||||
hasPushSupport,
|
||||
hasPushScheduledSupport,
|
||||
objectIdSize
|
||||
});
|
||||
hooksController,
|
||||
} = allControllers;
|
||||
this.config = Config.put(Object.assign({}, options, allControllers));
|
||||
|
||||
Config.validate(AppCache.get(appId));
|
||||
this.config = AppCache.get(appId);
|
||||
Config.setupPasswordValidator(this.config.passwordPolicy);
|
||||
logging.setLogger(loggerController);
|
||||
const dbInitPromise = databaseController.performInitialization();
|
||||
hooksController.load();
|
||||
|
||||
// Note: Tests will start to fail if any validation happens after this is called.
|
||||
@@ -303,30 +113,11 @@ class ParseServer {
|
||||
}
|
||||
}
|
||||
|
||||
getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
|
||||
let protocol;
|
||||
try {
|
||||
const parsedURI = url.parse(databaseURI);
|
||||
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
|
||||
} catch(e) { /* */ }
|
||||
switch (protocol) {
|
||||
case 'postgres:':
|
||||
return new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions
|
||||
});
|
||||
default:
|
||||
return new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get app() {
|
||||
return ParseServer.app(this.config);
|
||||
if (!this._app) {
|
||||
this._app = ParseServer.app(this.config);
|
||||
}
|
||||
return this._app;
|
||||
}
|
||||
|
||||
handleShutdown() {
|
||||
@@ -363,17 +154,17 @@ class ParseServer {
|
||||
// run the following when not testing
|
||||
if (!process.env.TESTING) {
|
||||
//This causes tests to spew some useless warnings, so disable in test
|
||||
/* istanbul ignore next */
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (err.code === "EADDRINUSE") { // user-friendly message for this common error
|
||||
/* eslint-disable no-console */
|
||||
console.error(`Unable to listen on port ${err.port}. The port is already in use.`);
|
||||
/* eslint-enable no-console */
|
||||
process.stderr.write(`Unable to listen on port ${err.port}. The port is already in use.`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
// verify the server url after a 'mount' event is received
|
||||
/* istanbul ignore next */
|
||||
api.on('mount', function() {
|
||||
ParseServer.verifyServerUrl();
|
||||
});
|
||||
@@ -415,7 +206,44 @@ class ParseServer {
|
||||
return appRouter;
|
||||
}
|
||||
|
||||
static createLiveQueryServer(httpServer, config) {
|
||||
start(options: ParseServerOptions, callback: ?()=>void) {
|
||||
const app = express();
|
||||
if (options.middleware) {
|
||||
let middleware;
|
||||
if (typeof options.middleware == 'string') {
|
||||
middleware = require(path.resolve(process.cwd(), options.middleware));
|
||||
} else {
|
||||
middleware = options.middleware; // use as-is let express fail
|
||||
}
|
||||
app.use(middleware);
|
||||
}
|
||||
|
||||
app.use(options.mountPath, this.app);
|
||||
const server = app.listen(options.port, options.host, callback);
|
||||
this.server = server;
|
||||
|
||||
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
|
||||
this.liveQueryServer = ParseServer.createLiveQueryServer(server, options.liveQueryServerOptions);
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.TESTING) {
|
||||
configureListeners(this);
|
||||
}
|
||||
this.expressApp = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
static start(options: ParseServerOptions, callback: ?()=>void) {
|
||||
const parseServer = new ParseServer(options);
|
||||
return parseServer.start(options, callback);
|
||||
}
|
||||
|
||||
static createLiveQueryServer(httpServer, config: LiveQueryServerOptions) {
|
||||
if (!httpServer || (config && config.port)) {
|
||||
var app = express();
|
||||
httpServer = require('http').createServer(app);
|
||||
httpServer.listen(config.port);
|
||||
}
|
||||
return new ParseLiveQueryServer(httpServer, config);
|
||||
}
|
||||
|
||||
@@ -447,4 +275,59 @@ function addParseCloud() {
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
function injectDefaults(options: ParseServerOptions) {
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
if (!options.hasOwnProperty(key)) {
|
||||
options[key] = defaults[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (!options.hasOwnProperty('serverURL')) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
options.userSensitiveFields = Array.from(new Set(options.userSensitiveFields.concat(
|
||||
defaults.userSensitiveFields,
|
||||
options.userSensitiveFields
|
||||
)));
|
||||
|
||||
options.masterKeyIps = Array.from(new Set(options.masterKeyIps.concat(
|
||||
defaults.masterKeyIps,
|
||||
options.masterKeyIps
|
||||
)));
|
||||
}
|
||||
|
||||
// Those can't be tested as it requires a subprocess
|
||||
/* istanbul ignore next */
|
||||
function configureListeners(parseServer) {
|
||||
const server = parseServer.server;
|
||||
const sockets = {};
|
||||
/* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
|
||||
This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */
|
||||
server.on('connection', (socket) => {
|
||||
const socketId = socket.remoteAddress + ':' + socket.remotePort;
|
||||
sockets[socketId] = socket;
|
||||
socket.on('close', () => {
|
||||
delete sockets[socketId];
|
||||
});
|
||||
});
|
||||
|
||||
const destroyAliveConnections = function() {
|
||||
for (const socketId in sockets) {
|
||||
try {
|
||||
sockets[socketId].destroy();
|
||||
} catch (e) { /* */ }
|
||||
}
|
||||
}
|
||||
|
||||
const handleShutdown = function() {
|
||||
process.stdout.write('Termination signal received. Shutting down.');
|
||||
destroyAliveConnections();
|
||||
server.close();
|
||||
parseServer.handleShutdown();
|
||||
};
|
||||
process.on('SIGTERM', handleShutdown);
|
||||
process.on('SIGINT', handleShutdown);
|
||||
}
|
||||
|
||||
export default ParseServer;
|
||||
|
||||
Reference in New Issue
Block a user