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

@@ -34,7 +34,8 @@
"jequal": true,
"create": true,
"arrayContains": true,
"databaseAdapter": true
"databaseAdapter": true,
"databaseURI": true
},
"rules": {
"no-console": [0],

View File

@@ -219,17 +219,14 @@ describe('execution', () => {
}
});
it('shoud start Parse Server', done => {
childProcess = spawn(binPath, [
'--appId',
'test',
'--masterKey',
'test',
'--databaseURI',
'mongodb://localhost/test',
'--port',
'1339',
]);
it('should start Parse Server', done => {
const env = { ...process.env };
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
childProcess = spawn(
binPath,
['--appId', 'test', '--masterKey', 'test', '--databaseURI', databaseURI, '--port', '1339'],
{ env }
);
childProcess.stdout.on('data', data => {
data = data.toString();
if (data.includes('parse-server running on')) {
@@ -241,18 +238,24 @@ describe('execution', () => {
});
});
it('shoud start Parse Server with GraphQL', done => {
childProcess = spawn(binPath, [
'--appId',
'test',
'--masterKey',
'test',
'--databaseURI',
'mongodb://localhost/test',
'--port',
'1340',
'--mountGraphQL',
]);
it('should start Parse Server with GraphQL', async done => {
const env = { ...process.env };
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
childProcess = spawn(
binPath,
[
'--appId',
'test',
'--masterKey',
'test',
'--databaseURI',
databaseURI,
'--port',
'1340',
'--mountGraphQL',
],
{ env }
);
let output = '';
childProcess.stdout.on('data', data => {
data = data.toString();
@@ -267,19 +270,25 @@ describe('execution', () => {
});
});
it('shoud start Parse Server with GraphQL and Playground', done => {
childProcess = spawn(binPath, [
'--appId',
'test',
'--masterKey',
'test',
'--databaseURI',
'mongodb://localhost/test',
'--port',
'1341',
'--mountGraphQL',
'--mountPlayground',
]);
it('should start Parse Server with GraphQL and Playground', async done => {
const env = { ...process.env };
env.NODE_OPTIONS = '--dns-result-order=ipv4first';
childProcess = spawn(
binPath,
[
'--appId',
'test',
'--masterKey',
'test',
'--databaseURI',
databaseURI,
'--port',
'1341',
'--mountGraphQL',
'--mountPlayground',
],
{ env }
);
let output = '';
childProcess.stdout.on('data', data => {
data = data.toString();

View File

@@ -1,6 +1,7 @@
'use strict';
const Config = require('../lib/Config');
const Parse = require('parse/node');
const ParseServer = require('../lib/index').ParseServer;
const request = require('../lib/request');
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
.InMemoryCacheAdapter;
@@ -39,6 +40,47 @@ describe('Cloud Code', () => {
});
});
it('can load cloud code as a module', async () => {
process.env.npm_package_type = 'module';
await reconfigureServer({ appId: 'test1', cloud: './spec/cloud/cloudCodeModuleFile.js' });
const result = await Parse.Cloud.run('cloudCodeInFile');
expect(result).toEqual('It is possible to define cloud code in a file.');
delete process.env.npm_package_type;
});
it('cloud code must be valid type', async () => {
await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith(
"argument 'cloud' must either be a string or a function"
);
});
it('should wait for cloud code to load', async () => {
await reconfigureServer({ appId: 'test3' });
const initiated = new Date();
const parseServer = await new ParseServer({
...defaultConfiguration,
appId: 'test3',
masterKey: 'test',
serverURL: 'http://localhost:12668/parse',
async cloud() {
await new Promise(resolve => setTimeout(resolve, 1000));
Parse.Cloud.beforeSave('Test', () => {
throw 'Cannot save.';
});
},
}).start();
const express = require('express');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12668);
const now = new Date();
expect(now.getTime() - initiated.getTime() > 1000).toBeTrue();
await expectAsync(new Parse.Object('Test').save()).toBeRejectedWith(
new Parse.Error(141, 'Cannot save.')
);
await new Promise(resolve => server.close(resolve));
});
it('can create functions', done => {
Parse.Cloud.define('hello', () => {
return 'Hello world!';

View File

@@ -631,7 +631,7 @@ describe('DefinedSchemas', () => {
const logger = require('../lib/logger').logger;
spyOn(DefinedSchemas.prototype, 'wait').and.resolveTo();
spyOn(logger, 'error').and.callThrough();
spyOn(Parse.Schema, 'all').and.callFake(() => {
spyOn(DefinedSchemas.prototype, 'createDeleteSession').and.callFake(() => {
throw error;
});

View File

@@ -115,8 +115,8 @@ describe('ParseLiveQueryServer', function () {
});
describe_only_db('mongo')('initialization', () => {
it('can be initialized through ParseServer without liveQueryServerOptions', async function (done) {
const parseServer = await ParseServer.start({
it('can be initialized through ParseServer without liveQueryServerOptions', async () => {
const parseServer = await ParseServer.startApp({
appId: 'hello',
masterKey: 'world',
port: 22345,
@@ -126,19 +126,14 @@ describe('ParseLiveQueryServer', function () {
classNames: ['Yolo'],
},
startLiveQueryServer: true,
serverStartComplete: () => {
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
parseServer.server.close(async () => {
await reconfigureServer();
done();
});
},
});
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
await new Promise(resolve => parseServer.server.close(resolve));
});
it('can be initialized through ParseServer with liveQueryServerOptions', async function (done) {
const parseServer = await ParseServer.start({
it('can be initialized through ParseServer with liveQueryServerOptions', async () => {
const parseServer = await ParseServer.startApp({
appId: 'hello',
masterKey: 'world',
port: 22346,
@@ -150,17 +145,10 @@ describe('ParseLiveQueryServer', function () {
liveQueryServerOptions: {
port: 22347,
},
serverStartComplete: () => {
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
parseServer.liveQueryServer.server.close(
parseServer.server.close.bind(parseServer.server, async () => {
await reconfigureServer();
done();
})
);
},
});
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
await new Promise(resolve => parseServer.server.close(resolve));
});
});

View File

@@ -92,7 +92,7 @@ describe('Server Url Checks', () => {
close = true;
},
});
const parseServer = ParseServer.start(newConfiguration);
const parseServer = ParseServer.startApp(newConfiguration);
});
it('does not have unhandled promise rejection in the case of load error', done => {

View File

@@ -0,0 +1,3 @@
Parse.Cloud.define('cloudCodeInFile', () => {
return 'It is possible to define cloud code in a file.';
});

View File

@@ -50,16 +50,19 @@ const { VolatileClassesSchemas } = require('../lib/Controllers/SchemaController'
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
let databaseAdapter;
let databaseURI;
// need to bind for mocking mocha
if (process.env.PARSE_SERVER_TEST_DB === 'postgres') {
databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI;
databaseAdapter = new PostgresStorageAdapter({
uri: process.env.PARSE_SERVER_TEST_DATABASE_URI || postgresURI,
uri: databaseURI,
collectionPrefix: 'test_',
});
} else {
databaseURI = mongoURI;
databaseAdapter = new MongoStorageAdapter({
uri: mongoURI,
uri: databaseURI,
collectionPrefix: 'test_',
});
}
@@ -149,49 +152,33 @@ let server;
let didChangeConfiguration = false;
// Allows testing specific configurations of Parse Server
const reconfigureServer = (changedConfiguration = {}) => {
return new Promise((resolve, reject) => {
if (server) {
return server.close(() => {
server = undefined;
reconfigureServer(changedConfiguration).then(resolve, reject);
});
}
try {
let parseServer = undefined;
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
serverStartComplete: error => {
if (error) {
reject(error);
} else {
Parse.CoreManager.setRESTController(RESTController);
resolve(parseServer);
}
},
mountPath: '/1',
port,
});
cache.clear();
ParseServer.start(newConfiguration).then(_parseServer => {
parseServer = _parseServer;
parseServer.expressApp.use('/1', err => {
console.error(err);
fail('should not call next');
});
server = parseServer.server;
server.on('connection', connection => {
const key = `${connection.remoteAddress}:${connection.remotePort}`;
openConnections[key] = connection;
connection.on('close', () => {
delete openConnections[key];
});
});
});
} catch (error) {
reject(error);
}
const reconfigureServer = async (changedConfiguration = {}) => {
if (server) {
await new Promise(resolve => server.close(resolve));
server = undefined;
return reconfigureServer(changedConfiguration);
}
didChangeConfiguration = Object.keys(changedConfiguration).length !== 0;
const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, {
mountPath: '/1',
port,
});
cache.clear();
const parseServer = await ParseServer.startApp(newConfiguration);
server = parseServer.server;
Parse.CoreManager.setRESTController(RESTController);
parseServer.expressApp.use('/1', err => {
console.error(err);
fail('should not call next');
});
server.on('connection', connection => {
const key = `${connection.remoteAddress}:${connection.remotePort}`;
openConnections[key] = connection;
connection.on('close', () => {
delete openConnections[key];
});
});
return parseServer;
};
// Set up a Parse client to talk to our test API server
@@ -423,6 +410,7 @@ global.defaultConfiguration = defaultConfiguration;
global.mockCustomAuthenticator = mockCustomAuthenticator;
global.mockFacebookAuthenticator = mockFacebookAuthenticator;
global.databaseAdapter = databaseAdapter;
global.databaseURI = databaseURI;
global.jfail = function (err) {
fail(JSON.stringify(err));
};

View File

@@ -61,34 +61,19 @@ describe('server', () => {
});
});
it('fails if database is unreachable', done => {
reconfigureServer({
it('fails if database is unreachable', async () => {
const server = new ParseServer.default({
...defaultConfiguration,
databaseAdapter: new MongoStorageAdapter({
uri: 'mongodb://fake:fake@localhost:43605/drew3',
mongoOptions: {
serverSelectionTimeoutMS: 2000,
},
}),
}).catch(() => {
const config = Config.get('test');
config.schemaCache.clear();
//Need to use rest api because saving via JS SDK results in fail() not getting called
request({
method: 'POST',
url: 'http://localhost:8378/1/classes/NewClass',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: {},
}).then(fail, response => {
expect(response.status).toEqual(500);
const body = response.data;
expect(body.code).toEqual(1);
expect(body.message).toEqual('Internal server error.');
reconfigureServer().then(done, done);
});
});
const error = await server.start().catch(e => e);
expect(`${error}`.includes('MongoServerSelectionError')).toBeTrue();
await reconfigureServer();
});
describe('mail adapter', () => {
@@ -295,91 +280,47 @@ describe('server', () => {
});
});
it('can create a parse-server v1', done => {
it('can create a parse-server v1', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const parseServer = new ParseServer.default(
Object.assign({}, defaultConfiguration, {
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12666/parse',
serverStartComplete: () => {
expect(Parse.applicationId).toEqual('aTestApp');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12666);
const obj = new Parse.Object('AnObject');
let objId;
obj
.save()
.then(obj => {
objId = obj.id;
const q = new Parse.Query('AnObject');
return q.first();
})
.then(obj => {
expect(obj.id).toEqual(objId);
server.close(async () => {
await reconfigureServer();
done();
});
})
.catch(() => {
server.close(async () => {
await reconfigureServer();
done();
});
});
},
})
);
await parseServer.start();
expect(Parse.applicationId).toEqual('aTestApp');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12666);
const obj = new Parse.Object('AnObject');
await obj.save();
const query = await new Parse.Query('AnObject').first();
expect(obj.id).toEqual(query.id);
await new Promise(resolve => server.close(resolve));
});
it('can create a parse-server v2', done => {
let objId;
let server;
it('can create a parse-server v2', async () => {
await reconfigureServer({ appId: 'anOtherTestApp' });
const parseServer = ParseServer.ParseServer(
Object.assign({}, defaultConfiguration, {
appId: 'anOtherTestApp',
masterKey: 'anOtherTestMasterKey',
serverURL: 'http://localhost:12667/parse',
serverStartComplete: error => {
const promise = error ? Promise.reject(error) : Promise.resolve();
promise
.then(() => {
expect(Parse.applicationId).toEqual('anOtherTestApp');
const app = express();
app.use('/parse', parseServer);
server = app.listen(12667);
const obj = new Parse.Object('AnObject');
return obj.save();
})
.then(obj => {
objId = obj.id;
const q = new Parse.Query('AnObject');
return q.first();
})
.then(obj => {
expect(obj.id).toEqual(objId);
server.close(async () => {
await reconfigureServer();
done();
});
})
.catch(error => {
fail(JSON.stringify(error));
if (server) {
server.close(async () => {
await reconfigureServer();
done();
});
} else {
done();
}
});
},
})
);
expect(Parse.applicationId).toEqual('anOtherTestApp');
await parseServer.start();
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12667);
const obj = new Parse.Object('AnObject');
await obj.save();
const q = await new Parse.Query('AnObject').first();
expect(obj.id).toEqual(q.id);
await new Promise(resolve => server.close(resolve));
});
it('has createLiveQueryServer', done => {
@@ -558,6 +499,84 @@ describe('server', () => {
.catch(done.fail);
});
it('can call start', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const config = {
...defaultConfiguration,
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12701/parse',
};
const parseServer = new ParseServer.ParseServer(config);
await parseServer.start();
expect(Parse.applicationId).toEqual('aTestApp');
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12701);
const testObject = new Parse.Object('TestObject');
await expectAsync(testObject.save()).toBeResolved();
await new Promise(resolve => server.close(resolve));
});
it('start is required to mount', async () => {
await reconfigureServer({ appId: 'aTestApp' });
const config = {
...defaultConfiguration,
appId: 'aTestApp',
masterKey: 'aTestMasterKey',
serverURL: 'http://localhost:12701/parse',
};
const parseServer = new ParseServer.ParseServer(config);
expect(Parse.applicationId).toEqual('aTestApp');
expect(Parse.serverURL).toEqual('http://localhost:12701/parse');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12701);
const response = await request({
headers: {
'X-Parse-Application-Id': 'aTestApp',
},
method: 'POST',
url: 'http://localhost:12701/parse/classes/TestObject',
}).catch(e => new Parse.Error(e.data.code, e.data.error));
expect(response).toEqual(
new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid server state: initialized')
);
const health = await request({
url: 'http://localhost:12701/parse/health',
}).catch(e => e);
expect(health.data.status).toBe('initialized');
expect(health.status).toBe(503);
await new Promise(resolve => server.close(resolve));
});
it('can get starting state', async () => {
await reconfigureServer({ appId: 'test2', silent: false });
const parseServer = new ParseServer.ParseServer({
...defaultConfiguration,
appId: 'test2',
masterKey: 'abc',
serverURL: 'http://localhost:12668/parse',
async cloud() {
await new Promise(resolve => setTimeout(resolve, 2000));
},
});
const express = require('express');
const app = express();
app.use('/parse', parseServer.app);
const server = app.listen(12668);
const startingPromise = parseServer.start();
const health = await request({
url: 'http://localhost:12668/parse/health',
}).catch(e => e);
expect(health.data.status).toBe('starting');
expect(health.status).toBe(503);
expect(health.headers['retry-after']).toBe('1');
await startingPromise;
await new Promise(resolve => server.close(resolve));
});
it('should not fail when Google signin is introduced without the optional clientId', done => {
const jwt = require('jsonwebtoken');

View File

@@ -6,14 +6,20 @@ const ParseServer = require('../../lib/index').ParseServer;
const databaseURI = 'mongodb://doesnotexist:27017/parseServerMongoAdapterTestDatabase';
ParseServer.start({
appId: 'test',
masterKey: 'test',
databaseAdapter: new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: {
serverSelectionTimeoutMS: 2000,
},
}),
filesAdapter: new GridFSBucketAdapter(databaseURI),
});
(async () => {
try {
await ParseServer.startApp({
appId: 'test',
masterKey: 'test',
databaseAdapter: new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: {
serverSelectionTimeoutMS: 2000,
},
}),
filesAdapter: new GridFSBucketAdapter(databaseURI),
});
} catch (e) {
process.exit(1);
}
})();