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:
Daniel
2022-12-22 01:30:13 +11:00
committed by GitHub
parent db9941c5a6
commit 99fcf45e55
21 changed files with 494 additions and 310 deletions

View File

@@ -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);
}
/**