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:
Florent Vilmart
2017-10-23 08:43:05 -04:00
committed by GitHub
parent d29a4483d0
commit 9de4b8b2a7
55 changed files with 1462 additions and 874 deletions

View File

@@ -1,42 +1,2 @@
import {
numberParser
} from '../utils/parsers';
export default {
"appId": {
required: true,
help: "Required. This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId."
},
"masterKey": {
required: true,
help: "Required. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
},
"serverURL": {
required: true,
help: "Required. This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL."
},
"redisURL": {
help: "Optional. This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
},
"keyPairs": {
help: "Optional. A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details."
},
"websocketTimeout": {
help: "Optional. Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients. Defaults to 10 * 1000 ms (10 s).",
action: numberParser("websocketTimeout")
},
"cacheTimeout": {
help: "Optional. Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details. Defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
action: numberParser("cacheTimeout")
},
"logLevel": {
help: "Optional. This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE. Defaults to INFO.",
},
"port": {
env: "PORT",
help: "The port to run the ParseServer. defaults to 1337.",
default: 1337,
action: numberParser("port")
},
};
const LiveQueryServerOptions = require('../../Options/Definitions').LiveQueryServerOptions;
export default LiveQueryServerOptions;

View File

@@ -1,284 +1,2 @@
import {
numberParser,
numberOrBoolParser,
objectParser,
arrayParser,
moduleOrObjectParser,
booleanParser,
nullParser
} from '../utils/parsers';
export default {
"appId": {
env: "PARSE_SERVER_APPLICATION_ID",
help: "Your Parse Application ID",
required: true
},
"masterKey": {
env: "PARSE_SERVER_MASTER_KEY",
help: "Your Parse Master Key",
required: true
},
"masterKeyIps": {
env: "PARSE_SERVER_MASTER_KEY_IPS",
help: "Restrict masterKey to be used by only these ips. defaults to [] (allow all ips)",
default: []
},
"port": {
env: "PORT",
help: "The port to run the ParseServer. defaults to 1337.",
default: 1337,
action: numberParser("port")
},
"host": {
env: "PARSE_SERVER_HOST",
help: "The host to serve ParseServer on. defaults to 0.0.0.0",
default: '0.0.0.0',
},
"databaseURI": {
env: "PARSE_SERVER_DATABASE_URI",
help: "The full URI to your mongodb database"
},
"databaseOptions": {
env: "PARSE_SERVER_DATABASE_OPTIONS",
help: "Options to pass to the mongodb client",
action: objectParser
},
"collectionPrefix": {
env: "PARSE_SERVER_COLLECTION_PREFIX",
help: 'A collection prefix for the classes'
},
"serverURL": {
env: "PARSE_SERVER_URL",
help: "URL to your parse server with http:// or https://.",
},
"publicServerURL": {
env: "PARSE_PUBLIC_SERVER_URL",
help: "Public URL to your parse server with http:// or https://.",
},
"clientKey": {
env: "PARSE_SERVER_CLIENT_KEY",
help: "Key for iOS, MacOS, tvOS clients"
},
"javascriptKey": {
env: "PARSE_SERVER_JAVASCRIPT_KEY",
help: "Key for the Javascript SDK"
},
"restAPIKey": {
env: "PARSE_SERVER_REST_API_KEY",
help: "Key for REST calls"
},
"dotNetKey": {
env: "PARSE_SERVER_DOT_NET_KEY",
help: "Key for Unity and .Net SDK"
},
"webhookKey": {
env: "PARSE_SERVER_WEBHOOK_KEY",
help: "Key sent with outgoing webhook calls"
},
"cloud": {
env: "PARSE_SERVER_CLOUD_CODE_MAIN",
help: "Full path to your cloud code main.js"
},
"push": {
env: "PARSE_SERVER_PUSH",
help: "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications",
action: objectParser
},
"scheduledPush": {
env: "PARSE_SERVER_SCHEDULED_PUSH",
help: "Configuration for push scheduling. Defaults to false.",
action: booleanParser
},
"oauth": {
env: "PARSE_SERVER_OAUTH_PROVIDERS",
help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication",
action: objectParser
},
"auth": {
env: "PARSE_SERVER_AUTH_PROVIDERS",
help: "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication",
action: objectParser
},
"fileKey": {
env: "PARSE_SERVER_FILE_KEY",
help: "Key for your files",
},
"facebookAppIds": {
env: "PARSE_SERVER_FACEBOOK_APP_IDS",
help: "[DEPRECATED (use auth option)]",
action: function() {
throw 'facebookAppIds is deprecated, please use { auth: \
{facebook: \
{ appIds: [] } \
}\
}\
}';
}
},
"enableAnonymousUsers": {
env: "PARSE_SERVER_ENABLE_ANON_USERS",
help: "Enable (or disable) anon users, defaults to true",
action: booleanParser
},
"allowClientClassCreation": {
env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION",
help: "Enable (or disable) client class creation, defaults to true",
action: booleanParser
},
"mountPath": {
env: "PARSE_SERVER_MOUNT_PATH",
help: "Mount path for the server, defaults to /parse",
default: "/parse"
},
"filesAdapter": {
env: "PARSE_SERVER_FILES_ADAPTER",
help: "Adapter module for the files sub-system",
action: moduleOrObjectParser
},
"emailAdapter": {
env: "PARSE_SERVER_EMAIL_ADAPTER",
help: "Adapter module for the email sending",
action: moduleOrObjectParser
},
"verifyUserEmails": {
env: "PARSE_SERVER_VERIFY_USER_EMAILS",
help: "Enable (or disable) user email validation, defaults to false",
action: booleanParser
},
"preventLoginWithUnverifiedEmail": {
env: "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL",
help: "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false",
action: booleanParser
},
"emailVerifyTokenValidityDuration": {
env: "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION",
help: "Email verification token validity duration",
action: numberParser("emailVerifyTokenValidityDuration")
},
"accountLockout": {
env: "PARSE_SERVER_ACCOUNT_LOCKOUT",
help: "account lockout policy for failed login attempts",
action: objectParser
},
"passwordPolicy": {
env: "PARSE_SERVER_PASSWORD_POLICY",
help: "Password policy for enforcing password related rules",
action: objectParser
},
"appName": {
env: "PARSE_SERVER_APP_NAME",
help: "Sets the app name"
},
"loggerAdapter": {
env: "PARSE_SERVER_LOGGER_ADAPTER",
help: "Adapter module for the logging sub-system",
action: moduleOrObjectParser
},
"customPages": {
env: "PARSE_SERVER_CUSTOM_PAGES",
help: "custom pages for password validation and reset",
action: objectParser
},
"maxUploadSize": {
env: "PARSE_SERVER_MAX_UPLOAD_SIZE",
help: "Max file size for uploads.",
default: "20mb"
},
"userSensitiveFields": {
help: "Personally identifiable information fields in the user table the should be removed for non-authorized users.",
default: ["email"]
},
"sessionLength": {
env: "PARSE_SERVER_SESSION_LENGTH",
help: "Session duration, defaults to 1 year",
action: numberParser("sessionLength")
},
"maxLimit": {
env: "PARSE_SERVER_MAX_LIMIT",
help: "Max value for limit option on queries, defaults to unlimited",
action: numberParser("maxLimit")
},
"verbose": {
env: "VERBOSE",
help: "Set the logging to verbose"
},
"jsonLogs": {
env: "JSON_LOGS",
help: "Log as structured JSON objects"
},
"logLevel": {
env: "PARSE_SERVER_LOG_LEVEL",
help: "Sets the level for logs"
},
"logsFolder": {
env: "PARSE_SERVER_LOGS_FOLDER",
help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
action: nullParser
},
"silent": {
help: "Disables console output",
},
"revokeSessionOnPasswordReset": {
env: "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET",
help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",
action: booleanParser
},
"schemaCacheTTL": {
env: "PARSE_SERVER_SCHEMA_CACHE_TTL",
help: "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 0; disabled.",
action: numberParser("schemaCacheTTL"),
},
"enableSingleSchemaCache": {
env: "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE",
help: "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA. Defaults to false, i.e. unique schema cache per request.",
action: booleanParser
},
"cacheTTL": {
env: "PARSE_SERVER_CACHE_TTL",
help: "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)",
action: numberParser("cacheTTL"),
},
"cacheMaxSize": {
env: "PARSE_SERVER_CACHE_MAX_SIZE",
help: "Sets the maximum size for the in memory cache, defaults to 10000",
action: numberParser("cacheMaxSize")
},
"cluster": {
env: "PARSE_SERVER_CLUSTER",
help: "Run with cluster, optionally set the number of processes default to os.cpus().length",
action: numberOrBoolParser("cluster")
},
"liveQuery": {
env: "PARSE_SERVER_LIVE_QUERY_OPTIONS",
help: "parse-server's LiveQuery configuration object",
action: objectParser
},
"liveQuery.classNames": {
help: "parse-server's LiveQuery classNames",
action: arrayParser
},
"liveQuery.redisURL": {
help: "parse-server's LiveQuery redisURL",
},
"startLiveQueryServer": {
help: "Starts the liveQuery server",
action: booleanParser
},
"liveQueryPort": {
help: 'Specific port to start the live query server',
action: numberParser("liveQueryPort")
},
"liveQueryServerOptions": {
help: "Live query server configuration options (will start the liveQuery server)",
action: objectParser
},
"middleware": {
help: "middleware for express server, can be string or function"
},
"objectIdSize": {
env: "PARSE_SERVER_OBJECT_ID_SIZE",
help: "Sets the number of characters in generated object id's, default 10",
action: numberParser("objectIdSize")
}
};
const ParseServerDefinitions = require('../../Options/Definitions').ParseServerOptions;
export default ParseServerDefinitions;

View File

@@ -1,15 +1,11 @@
import definitions from './definitions/parse-live-query-server';
import runner from './utils/runner';
import { ParseServer } from '../index';
import express from 'express';
runner({
definitions,
start: function(program, options, logOptions) {
logOptions();
var app = express();
var httpServer = require('http').createServer(app);
httpServer.listen(options.port);
ParseServer.createLiveQueryServer(httpServer, options);
ParseServer.createLiveQueryServer(undefined, options);
}
})

View File

@@ -1,11 +1,9 @@
/* eslint-disable no-console */
import express from 'express';
import ParseServer from '../index';
import definitions from './definitions/parse-server';
import cluster from 'cluster';
import os from 'os';
import runner from './utils/runner';
const path = require("path");
const help = function(){
console.log(' Get Started guide:');
@@ -29,79 +27,12 @@ const help = function(){
console.log('');
};
function startServer(options, callback) {
const app = express();
if (options.middleware) {
let middleware;
if (typeof options.middleware == 'function') {
middleware = options.middleware;
} if (typeof options.middleware == 'string') {
middleware = require(path.resolve(process.cwd(), options.middleware));
} else {
throw "middleware should be a string or a function";
}
app.use(middleware);
}
const parseServer = new ParseServer(options);
const sockets = {};
app.use(options.mountPath, parseServer.app);
const server = app.listen(options.port, options.host, callback);
server.on('connection', initializeConnections);
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
let liveQueryServer = server;
if (options.liveQueryPort) {
liveQueryServer = express().listen(options.liveQueryPort, () => {
console.log('ParseLiveQuery listening on ' + options.liveQueryPort);
});
}
ParseServer.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions);
}
function initializeConnections(socket) {
/* 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. */
const socketId = socket.remoteAddress + ':' + socket.remotePort;
sockets[socketId] = socket;
socket.on('close', () => {
delete sockets[socketId];
});
}
function destroyAliveConnections() {
for (const socketId in sockets) {
try {
sockets[socketId].destroy();
} catch (e) { /* */ }
}
}
const handleShutdown = function() {
console.log('Termination signal received. Shutting down.');
destroyAliveConnections();
server.close();
parseServer.handleShutdown();
};
process.on('SIGTERM', handleShutdown);
process.on('SIGINT', handleShutdown);
}
runner({
definitions,
help,
usage: '[options] <path/to/configuration.json>',
start: function(program, options, logOptions) {
if (!options.serverURL) {
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
}
if (!options.appId || !options.masterKey || !options.serverURL) {
if (!options.appId || !options.masterKey) {
program.outputHelp();
console.error("");
console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m');
@@ -132,12 +63,12 @@ runner({
cluster.fork();
});
} else {
startServer(options, () => {
ParseServer.start(options, () => {
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);
});
}
} else {
startServer(options, () => {
ParseServer.start(options, () => {
logOptions();
console.log('');
console.log('[' + process.pid + '] parse-server running on ' + options.serverURL);

View File

@@ -20,13 +20,6 @@ Command.prototype.loadDefinitions = function(definitions) {
return program.option(`--${opt} [${opt}]`);
}, this);
_defaults = Object.keys(definitions).reduce((defs, opt) => {
if(_definitions[opt].default) {
defs[opt] = _definitions[opt].default;
}
return defs;
}, {});
_reverseDefinitions = Object.keys(definitions).reduce((object, key) => {
let value = definitions[key];
if (typeof value == "object") {
@@ -38,6 +31,13 @@ Command.prototype.loadDefinitions = function(definitions) {
return object;
}, {});
_defaults = Object.keys(definitions).reduce((defs, opt) => {
if(_definitions[opt].default) {
defs[opt] = _definitions[opt].default;
}
return defs;
}, {});
/* istanbul ignore next */
this.on('--help', function(){
console.log(' Configure From Environment:');

View File

@@ -1,65 +0,0 @@
export function numberParser(key) {
return function(opt) {
const intOpt = parseInt(opt);
if (!Number.isInteger(intOpt)) {
throw new Error(`Key ${key} has invalid value ${opt}`);
}
return intOpt;
}
}
export function numberOrBoolParser(key) {
return function(opt) {
if (typeof opt === 'boolean') {
return opt;
}
if (opt === 'true') {
return true;
}
if (opt === 'false') {
return false;
}
return numberParser(key)(opt);
}
}
export function objectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
return JSON.parse(opt)
}
export function arrayParser(opt) {
if (Array.isArray(opt)) {
return opt;
} else if (typeof opt === 'string') {
return opt.split(',');
} else {
throw new Error(`${opt} should be a comma separated string or an array`);
}
}
export function moduleOrObjectParser(opt) {
if (typeof opt == 'object') {
return opt;
}
try {
return JSON.parse(opt);
} catch(e) { /* */ }
return opt;
}
export function booleanParser(opt) {
if (opt == true || opt == 'true' || opt == '1') {
return true;
}
return false;
}
export function nullParser(opt) {
if (opt == 'null') {
return null;
}
return opt;
}

View File

@@ -8,7 +8,13 @@ function logStartupOptions(options) {
value = "***REDACTED***";
}
if (typeof value === 'object') {
value = JSON.stringify(value);
try {
value = JSON.stringify(value)
} catch(e) {
if (value && value.constructor && value.constructor.name) {
value = value.constructor.name;
}
}
}
/* eslint-disable no-console */
console.log(`${key}: ${value}`);