* Support for Aggregate Queries * improve pg and coverage * Mongo 3.4 aggregates and tests * replace _id with objectId * improve tests for objectId * project with group query * typo
347 lines
12 KiB
JavaScript
347 lines
12 KiB
JavaScript
// ParseServer - open-source compatible API Server for Parse apps
|
|
|
|
var batch = require('./batch'),
|
|
bodyParser = require('body-parser'),
|
|
express = require('express'),
|
|
middlewares = require('./middlewares'),
|
|
Parse = require('parse/node').Parse,
|
|
path = require('path');
|
|
|
|
import { ParseServerOptions,
|
|
LiveQueryServerOptions } from './Options';
|
|
import defaults from './defaults';
|
|
import * as logging from './logger';
|
|
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 { FilesRouter } from './Routers/FilesRouter';
|
|
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
|
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
|
import { HooksRouter } from './Routers/HooksRouter';
|
|
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
|
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
|
import { LogsRouter } from './Routers/LogsRouter';
|
|
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
|
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
|
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 { UsersRouter } from './Routers/UsersRouter';
|
|
import { PurgeRouter } from './Routers/PurgeRouter';
|
|
import { AudiencesRouter } from './Routers/AudiencesRouter';
|
|
import { AggregateRouter } from './Routers/AggregateRouter';
|
|
|
|
import { ParseServerRESTController } from './ParseServerRESTController';
|
|
import * as controllers from './Controllers';
|
|
// 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:
|
|
// "analyticsAdapter": an adapter class for analytics
|
|
// "filesAdapter": a class like GridStoreAdapter providing create, get,
|
|
// and delete
|
|
// "loggerAdapter": a class like WinstonLoggerAdapter providing info, error,
|
|
// and query
|
|
// "jsonLogs": log as structured JSON objects
|
|
// "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
|
|
// "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
|
|
// "webhookKey": optional key from Parse dashboard
|
|
// "javascriptKey": optional key from Parse dashboard
|
|
// "push": optional key from configure push
|
|
// "sessionLength": optional length in seconds for how long Sessions should be valid for
|
|
// "maxLimit": optional upper bound for what can be specified for the 'limit' parameter on queries
|
|
|
|
class ParseServer {
|
|
|
|
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;
|
|
|
|
const allControllers = controllers.getControllers(options);
|
|
|
|
const {
|
|
loggerController,
|
|
databaseController,
|
|
hooksController,
|
|
} = allControllers;
|
|
this.config = Config.put(Object.assign({}, options, allControllers));
|
|
|
|
logging.setLogger(loggerController);
|
|
const dbInitPromise = databaseController.performInitialization();
|
|
hooksController.load();
|
|
|
|
// Note: Tests will start to fail if any validation happens after this is called.
|
|
if (process.env.TESTING) {
|
|
__indexBuildCompletionCallbackForTests(dbInitPromise);
|
|
}
|
|
|
|
if (cloud) {
|
|
addParseCloud();
|
|
if (typeof cloud === 'function') {
|
|
cloud(Parse)
|
|
} else if (typeof cloud === 'string') {
|
|
require(path.resolve(process.cwd(), cloud));
|
|
} else {
|
|
throw "argument 'cloud' must either be a string or a function";
|
|
}
|
|
}
|
|
}
|
|
|
|
get app() {
|
|
if (!this._app) {
|
|
this._app = ParseServer.app(this.config);
|
|
}
|
|
return this._app;
|
|
}
|
|
|
|
handleShutdown() {
|
|
const { adapter } = this.config.databaseController;
|
|
if (adapter && typeof adapter.handleShutdown === 'function') {
|
|
adapter.handleShutdown();
|
|
}
|
|
}
|
|
|
|
static app({maxUploadSize = '20mb', appId}) {
|
|
// 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().expressRouter({
|
|
maxUploadSize: maxUploadSize
|
|
}));
|
|
|
|
api.use('/health', (function(req, res) {
|
|
res.json({
|
|
status: 'ok'
|
|
});
|
|
}));
|
|
|
|
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressRouter());
|
|
|
|
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
|
|
api.use(middlewares.allowCrossDomain);
|
|
api.use(middlewares.allowMethodOverride);
|
|
api.use(middlewares.handleParseHeaders);
|
|
|
|
const appRouter = ParseServer.promiseRouter({ appId });
|
|
api.use(appRouter.expressRouter());
|
|
|
|
api.use(middlewares.handleParseErrors);
|
|
|
|
// 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
|
|
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();
|
|
});
|
|
}
|
|
if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') {
|
|
Parse.CoreManager.setRESTController(ParseServerRESTController(appId, appRouter));
|
|
}
|
|
return api;
|
|
}
|
|
|
|
static promiseRouter({appId}) {
|
|
const 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(),
|
|
new GlobalConfigRouter(),
|
|
new PurgeRouter(),
|
|
new HooksRouter(),
|
|
new CloudCodeRouter(),
|
|
new AudiencesRouter(),
|
|
new AggregateRouter()
|
|
];
|
|
|
|
const routes = routers.reduce((memo, router) => {
|
|
return memo.concat(router.routes);
|
|
}, []);
|
|
|
|
const appRouter = new PromiseRouter(routes, appId);
|
|
|
|
batch.mountOnto(appRouter);
|
|
return appRouter;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static verifyServerUrl(callback) {
|
|
// perform a health check on the serverURL value
|
|
if(Parse.serverURL) {
|
|
const request = require('request');
|
|
request(Parse.serverURL.replace(/\/$/, "") + "/health", function (error, response, body) {
|
|
let json;
|
|
try {
|
|
json = JSON.parse(body);
|
|
} catch(e) {
|
|
json = null;
|
|
}
|
|
if (error || response.statusCode !== 200 || !json || json && json.status !== 'ok') {
|
|
/* eslint-disable no-console */
|
|
console.warn(`\nWARNING, Unable to connect to '${Parse.serverURL}'.` +
|
|
` Cloud code and push notifications may be unavailable!\n`);
|
|
/* eslint-enable no-console */
|
|
if(callback) {
|
|
callback(false);
|
|
}
|
|
} else {
|
|
if(callback) {
|
|
callback(true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function addParseCloud() {
|
|
const ParseCloud = require("./cloud-code/Parse.Cloud");
|
|
Object.assign(Parse.Cloud, ParseCloud);
|
|
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;
|