feat: Asynchronous initialization of Parse Server (#8232)
BREAKING CHANGE: This release introduces the asynchronous initialization of Parse Server to prevent mounting Parse Server before being ready to receive request; it changes how Parse Server is imported, initialized and started; it also removes the callback `serverStartComplete`; see the [Parse Server 6 migration guide](https://github.com/parse-community/parse-server/blob/alpha/6.0.0.md) for more details (#8232)
This commit is contained in:
@@ -466,10 +466,6 @@ module.exports.ParseServerOptions = {
|
||||
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
|
||||
help: 'Callback when server has closed',
|
||||
},
|
||||
serverStartComplete: {
|
||||
env: 'PARSE_SERVER_SERVER_START_COMPLETE',
|
||||
help: 'Callback when server has started',
|
||||
},
|
||||
serverURL: {
|
||||
env: 'PARSE_SERVER_URL',
|
||||
help: 'URL to your parse server with http:// or https://.',
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
* @property {SchemaOptions} schema Defined schema
|
||||
* @property {SecurityOptions} security The security options to identify and report weak security settings.
|
||||
* @property {Function} serverCloseComplete Callback when server has closed
|
||||
* @property {Function} serverStartComplete Callback when server has started
|
||||
* @property {String} serverURL URL to your parse server with http:// or https://.
|
||||
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
|
||||
* @property {Boolean} silent Disables console output
|
||||
|
||||
@@ -271,8 +271,6 @@ export interface ParseServerOptions {
|
||||
:ENV: PARSE_SERVER_PLAYGROUND_PATH
|
||||
:DEFAULT: /playground */
|
||||
playgroundPath: ?string;
|
||||
/* Callback when server has started */
|
||||
serverStartComplete: ?(error: ?Error) => void;
|
||||
/* Defined schema
|
||||
:ENV: PARSE_SERVER_SCHEMA
|
||||
*/
|
||||
|
||||
@@ -64,73 +64,85 @@ class ParseServer {
|
||||
const {
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
cloud,
|
||||
security,
|
||||
javascriptKey,
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
serverStartComplete,
|
||||
schema,
|
||||
} = 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,
|
||||
liveQueryController,
|
||||
} = allControllers;
|
||||
options.state = 'initialized';
|
||||
this.config = Config.put(Object.assign({}, options, allControllers));
|
||||
logging.setLogger(allControllers.loggerController);
|
||||
}
|
||||
|
||||
logging.setLogger(loggerController);
|
||||
/**
|
||||
* Starts Parse Server as an express app; this promise resolves when Parse Server is ready to accept requests.
|
||||
*/
|
||||
|
||||
// Note: Tests will start to fail if any validation happens after this is called.
|
||||
databaseController
|
||||
.performInitialization()
|
||||
.then(() => hooksController.load())
|
||||
.then(async () => {
|
||||
const startupPromises = [];
|
||||
if (schema) {
|
||||
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
|
||||
}
|
||||
if (
|
||||
options.cacheAdapter &&
|
||||
options.cacheAdapter.connect &&
|
||||
typeof options.cacheAdapter.connect === 'function'
|
||||
) {
|
||||
startupPromises.push(options.cacheAdapter.connect());
|
||||
}
|
||||
startupPromises.push(liveQueryController.connect());
|
||||
await Promise.all(startupPromises);
|
||||
if (serverStartComplete) {
|
||||
serverStartComplete();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (serverStartComplete) {
|
||||
serverStartComplete(error);
|
||||
} else {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
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";
|
||||
async start() {
|
||||
try {
|
||||
if (this.config.state === 'ok') {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
if (security && security.enableCheck && security.enableCheckLog) {
|
||||
new CheckRunner(options.security).run();
|
||||
this.config.state = 'starting';
|
||||
Config.put(this.config);
|
||||
const {
|
||||
databaseController,
|
||||
hooksController,
|
||||
cloud,
|
||||
security,
|
||||
schema,
|
||||
cacheAdapter,
|
||||
liveQueryController,
|
||||
} = this.config;
|
||||
try {
|
||||
await databaseController.performInitialization();
|
||||
} catch (e) {
|
||||
if (e.code !== Parse.Error.DUPLICATE_VALUE) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await hooksController.load();
|
||||
const startupPromises = [];
|
||||
if (schema) {
|
||||
startupPromises.push(new DefinedSchemas(schema, this.config).execute());
|
||||
}
|
||||
if (cacheAdapter?.connect && typeof cacheAdapter.connect === 'function') {
|
||||
startupPromises.push(cacheAdapter.connect());
|
||||
}
|
||||
startupPromises.push(liveQueryController.connect());
|
||||
await Promise.all(startupPromises);
|
||||
if (cloud) {
|
||||
addParseCloud();
|
||||
if (typeof cloud === 'function') {
|
||||
await Promise.resolve(cloud(Parse));
|
||||
} else if (typeof cloud === 'string') {
|
||||
let json;
|
||||
if (process.env.npm_package_json) {
|
||||
json = require(process.env.npm_package_json);
|
||||
}
|
||||
if (process.env.npm_package_type === 'module' || json?.type === 'module') {
|
||||
await import(path.resolve(process.cwd(), cloud)).default;
|
||||
} else {
|
||||
require(path.resolve(process.cwd(), cloud));
|
||||
}
|
||||
} else {
|
||||
throw "argument 'cloud' must either be a string or a function";
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
if (security && security.enableCheck && security.enableCheckLog) {
|
||||
new CheckRunner(security).run();
|
||||
}
|
||||
this.config.state = 'ok';
|
||||
Config.put(this.config);
|
||||
return this;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.config.state = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +194,12 @@ class ParseServer {
|
||||
);
|
||||
|
||||
api.use('/health', function (req, res) {
|
||||
res.status(options.state === 'ok' ? 200 : 503);
|
||||
if (options.state === 'starting') {
|
||||
res.set('Retry-After', 1);
|
||||
}
|
||||
res.json({
|
||||
status: 'ok',
|
||||
status: options.state,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,10 +282,16 @@ class ParseServer {
|
||||
/**
|
||||
* starts the parse server's express app
|
||||
* @param {ParseServerOptions} options to use to start the server
|
||||
* @param {Function} callback called when the server has started
|
||||
* @returns {ParseServer} the parse server instance
|
||||
*/
|
||||
async start(options: ParseServerOptions, callback: ?() => void) {
|
||||
|
||||
async startApp(options: ParseServerOptions) {
|
||||
try {
|
||||
await this.start();
|
||||
} catch (e) {
|
||||
console.error('Error on ParseServer.startApp: ', e);
|
||||
throw e;
|
||||
}
|
||||
const app = express();
|
||||
if (options.middleware) {
|
||||
let middleware;
|
||||
@@ -280,7 +302,6 @@ class ParseServer {
|
||||
}
|
||||
app.use(middleware);
|
||||
}
|
||||
|
||||
app.use(options.mountPath, this.app);
|
||||
|
||||
if (options.mountGraphQL === true || options.mountPlayground === true) {
|
||||
@@ -308,8 +329,11 @@ class ParseServer {
|
||||
parseGraphQLServer.applyPlayground(app);
|
||||
}
|
||||
}
|
||||
|
||||
const server = app.listen(options.port, options.host, callback);
|
||||
const server = await new Promise(resolve => {
|
||||
app.listen(options.port, options.host, function () {
|
||||
resolve(this);
|
||||
});
|
||||
});
|
||||
this.server = server;
|
||||
|
||||
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
|
||||
@@ -330,12 +354,11 @@ class ParseServer {
|
||||
/**
|
||||
* Creates a new ParseServer and starts it.
|
||||
* @param {ParseServerOptions} options used to start the server
|
||||
* @param {Function} callback called when the server has started
|
||||
* @returns {ParseServer} the parse server instance
|
||||
*/
|
||||
static start(options: ParseServerOptions, callback: ?() => void) {
|
||||
static async startApp(options: ParseServerOptions) {
|
||||
const parseServer = new ParseServer(options);
|
||||
return parseServer.start(options, callback);
|
||||
return parseServer.startApp(options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,8 @@ import { internalCreateSchema, internalUpdateSchema } from '../Routers/SchemasRo
|
||||
import { defaultColumns, systemClasses } from '../Controllers/SchemaController';
|
||||
import { ParseServerOptions } from '../Options';
|
||||
import * as Migrations from './Migrations';
|
||||
import Auth from '../Auth';
|
||||
import rest from '../rest';
|
||||
|
||||
export class DefinedSchemas {
|
||||
config: ParseServerOptions;
|
||||
@@ -96,9 +98,10 @@ export class DefinedSchemas {
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
// Hack to force session schema to be created
|
||||
await this.createDeleteSession();
|
||||
this.allCloudSchemas = await Parse.Schema.all();
|
||||
// @flow-disable-next-line
|
||||
const schemaController = await this.config.database.loadSchema();
|
||||
this.allCloudSchemas = await schemaController.getAllClasses();
|
||||
clearTimeout(timeout);
|
||||
await Promise.all(this.localSchemas.map(async localSchema => this.saveOrUpdate(localSchema)));
|
||||
|
||||
@@ -171,9 +174,8 @@ export class DefinedSchemas {
|
||||
// Create a fake session since Parse do not create the _Session until
|
||||
// a session is created
|
||||
async createDeleteSession() {
|
||||
const session = new Parse.Session();
|
||||
await session.save(null, { useMasterKey: true });
|
||||
await session.destroy({ useMasterKey: true });
|
||||
const { response } = await rest.create(this.config, Auth.master(this.config), '_Session', {});
|
||||
await rest.del(this.config, Auth.master(this.config), '_Session', response.objectId);
|
||||
}
|
||||
|
||||
async saveOrUpdate(localSchema: Migrations.JSONSchema) {
|
||||
|
||||
@@ -68,16 +68,26 @@ runner({
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
ParseServer.start(options, () => {
|
||||
printSuccessMessage();
|
||||
});
|
||||
ParseServer.startApp(options)
|
||||
.then(() => {
|
||||
printSuccessMessage();
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ParseServer.start(options, () => {
|
||||
logOptions();
|
||||
console.log('');
|
||||
printSuccessMessage();
|
||||
});
|
||||
ParseServer.startApp(options)
|
||||
.then(() => {
|
||||
logOptions();
|
||||
console.log('');
|
||||
printSuccessMessage();
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
function printSuccessMessage() {
|
||||
|
||||
@@ -16,11 +16,11 @@ import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer';
|
||||
// Factory function
|
||||
const _ParseServer = function (options: ParseServerOptions) {
|
||||
const server = new ParseServer(options);
|
||||
return server.app;
|
||||
return server;
|
||||
};
|
||||
// Mount the create liveQueryServer
|
||||
_ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer;
|
||||
_ParseServer.start = ParseServer.start;
|
||||
_ParseServer.startApp = ParseServer.startApp;
|
||||
|
||||
const S3Adapter = useExternal('S3Adapter', '@parse/s3-files-adapter');
|
||||
const GCSAdapter = useExternal('GCSAdapter', '@parse/gcs-files-adapter');
|
||||
|
||||
@@ -158,9 +158,18 @@ export function handleParseHeaders(req, res, next) {
|
||||
}
|
||||
|
||||
const clientIp = getClientIp(req);
|
||||
const config = Config.get(info.appId, mount);
|
||||
if (config.state && config.state !== 'ok') {
|
||||
res.status(500);
|
||||
res.json({
|
||||
code: Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
error: `Invalid server state: ${config.state}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
info.app = AppCache.get(info.appId);
|
||||
req.config = Config.get(info.appId, mount);
|
||||
req.config = config;
|
||||
req.config.headers = req.headers || {};
|
||||
req.config.ip = clientIp;
|
||||
req.info = info;
|
||||
|
||||
Reference in New Issue
Block a user