feat: Increase required minimum MongoDB versions to 6.0.19, 7.0.16, 8.0.4 (#9531)

BREAKING CHANGE: This releases increases the required minimum MongoDB versions to `6.0.19`, `7.0.16`, `8.0.4` and removes support for MongoDB `4`, `5`.
This commit is contained in:
Manuel
2025-01-12 01:44:10 +01:00
committed by GitHub
parent fbf78f0802
commit 871e5082a9
5 changed files with 31 additions and 284 deletions

View File

@@ -142,41 +142,29 @@ jobs:
strategy:
matrix:
include:
- name: MongoDB 4.2, ReplicaSet
MONGODB_VERSION: 4.2.25
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: MongoDB 4.4, ReplicaSet
MONGODB_VERSION: 4.4.29
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: MongoDB 5, ReplicaSet
MONGODB_VERSION: 5.0.26
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: MongoDB 6, ReplicaSet
MONGODB_VERSION: 6.0.14
MONGODB_VERSION: 6.0.19
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: MongoDB 7, ReplicaSet
MONGODB_VERSION: 7.0.8
MONGODB_VERSION: 7.0.16
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: MongoDB 8, ReplicaSet
MONGODB_VERSION: 8.0.0
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: replset
NODE_VERSION: 22.12.0
- name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 8.0.0
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
NODE_VERSION: 22.12.0
- name: Node 20
MONGODB_VERSION: 8.0.0
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
NODE_VERSION: 20.18.0
- name: Node 18
MONGODB_VERSION: 8.0.0
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
NODE_VERSION: 18.20.4
fail-fast: false

View File

@@ -127,24 +127,21 @@ Before you start make sure you have installed:
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
| Version | Latest Version | End-of-Life | Compatible |
|------------|----------------|-------------|------------|
| Node.js 18 | 18.20.4 | April 2025 | ✅ Yes |
| Node.js 20 | 20.18.0 | April 2026 | ✅ Yes |
| Node.js 22 | 22.12.0 | April 2027 | ✅ Yes |
| Version | Minimum Version | End-of-Life | Compatible |
|------------|-----------------|-------------|------------|
| Node.js 18 | 18.20.4 | April 2025 | ✅ Yes |
| Node.js 20 | 20.18.0 | April 2026 | ✅ Yes |
| Node.js 22 | 22.12.0 | April 2027 | ✅ Yes |
#### MongoDB
Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and [MongoDB lifecycle schedule](https://www.mongodb.com/support-policy/lifecycles) and only test against versions that are officially supported and have not reached their end-of-life date. MongoDB "rapid releases" are ignored as these are considered pre-releases of the next major version.
| Version | Latest Version | End-of-Life | Compatible |
|-------------|----------------|---------------|------------|
| MongoDB 4.2 | 4.2.25 | April 2023 | ✅ Yes |
| MongoDB 4.4 | 4.4.29 | February 2024 | ✅ Yes |
| MongoDB 5 | 5.0.26 | October 2024 | ✅ Yes |
| MongoDB 6 | 6.0.14 | July 2025 | ✅ Yes |
| MongoDB 7 | 7.0.8 | TDB | ✅ Yes |
| MongoDB 8 | 8.0.0 | TDB | ✅ Yes |
| Version | Minimum Version | End-of-Life | Compatible |
|-----------|-----------------|-------------|------------|
| MongoDB 6 | 6.0.19 | July 2025 | ✅ Yes |
| MongoDB 7 | 7.0.16 | August 2026 | ✅ Yes |
| MongoDB 8 | 8.0.4 | TDB | ✅ Yes |
#### PostgreSQL

View File

@@ -121,18 +121,15 @@
"test:mongodb:runnerstart": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"test:mongodb:testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=$npm_config_dbversion} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test:mongodb": "npm run test:mongodb:runnerstart --dbversion=$npm_config_dbversion && npm run test:mongodb:testonly --dbversion=$npm_config_dbversion",
"test:mongodb:4.2.19": "npm run test:mongodb --dbversion=4.2.19",
"test:mongodb:4.4.13": "npm run test:mongodb --dbversion=4.4.13",
"test:mongodb:5.3.2": "npm run test:mongodb --dbversion=5.3.2",
"test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2",
"test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1",
"test:mongodb:8.0.3": "npm run test:mongodb --dbversion=8.0.3",
"test:mongodb:6.0.19": "npm run test:mongodb --dbversion=6.0.19",
"test:mongodb:7.0.16": "npm run test:mongodb --dbversion=7.0.16",
"test:mongodb:8.0.4": "npm run test:mongodb --dbversion=8.0.4",
"test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017",
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine",
"test": "npm run testonly",
"posttest": "cross-env mongodb-runner stop --all",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=8.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine",
"start": "node ./bin/parse-server",
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
"prepare": "npm run build",

View File

@@ -424,40 +424,6 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
});
it_only_mongodb_version('>=5.1 <6')('should use index for caseInsensitive query', async () => {
const user = new Parse.User();
user.set('username', 'Bugs');
user.set('password', 'Bunny');
await user.signUp();
const database = Config.get(Parse.applicationId).database;
await database.adapter.dropAllIndexes('_User');
const preIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
const schema = await new Parse.Schema('_User').get();
await database.adapter.ensureIndex(
'_User',
schema,
['username'],
'case_insensitive_username',
true
);
const postIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
});
it('should delete field without index', async () => {
const database = Config.get(Parse.applicationId).database;
const obj = new Parse.Object('MyObject');

View File

@@ -39,17 +39,6 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.1 <6')('query find with hint string', async () => {
const object = new TestObject();
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
const explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=8')('query find with hint string', async () => {
const object = new TestObject();
await object.save();
@@ -76,20 +65,6 @@ describe_only_db('mongo')('Parse.Query hint', () => {
});
});
it_only_mongodb_version('>=5.1 <6')('query find with hint object', async () => {
const object = new TestObject();
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
const explain = await collection._rawFind(
{ _id: object.id },
{ hint: { _id: 1 }, explain: true }
);
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=8')('query find with hint object', async () => {
const object = new TestObject();
await object.save();
@@ -104,7 +79,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
});
});
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
it_only_mongodb_version('<7')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -112,29 +87,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: '_id_',
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=4.4 <5.1')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
let queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
@@ -150,31 +103,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.1 <5.2')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: '_id_',
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.2')('query aggregate with hint string', async () => {
it_only_mongodb_version('>=7')('query aggregate with hint string', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -198,7 +127,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
it_only_mongodb_version('<7')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -206,27 +135,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: { _id: 1 },
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=4.4 <5.1')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
let queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
@@ -243,32 +152,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.1 <5.2')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
const collection = await config.database.adapter._adaptiveCollection('TestObject');
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
explain: true,
});
let { queryPlanner } = result[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
hint: { _id: 1 },
explain: true,
});
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.2')('query aggregate with hint object', async () => {
it_only_mongodb_version('>=7')('query aggregate with hint object', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
@@ -318,32 +202,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('>=5.1 <6')('query find with hint (rest)', async () => {
const object = new TestObject();
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/classes/TestObject',
qs: {
explain: true,
},
});
let response = await request(options);
let explain = response.data.results;
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/classes/TestObject',
qs: {
explain: true,
hint: '_id_',
},
});
response = await request(options);
explain = response.data.results;
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
});
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
it_only_mongodb_version('<7')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
@@ -354,34 +213,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
},
});
let response = await request(options);
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
hint: '_id_',
$group: JSON.stringify({ _id: '$foo' }),
},
});
response = await request(options);
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=4.4 <5.1')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
$group: JSON.stringify({ _id: '$foo' }),
},
});
let response = await request(options);
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
let queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.inputStage.inputStage).toBeUndefined();
@@ -403,40 +235,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.1 <5.2')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
$group: JSON.stringify({ _id: '$foo' }),
},
});
let response = await request(options);
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
options = Object.assign({}, masterKeyOptions, {
url: Parse.serverURL + '/aggregate/TestObject',
qs: {
explain: true,
hint: '_id_',
$group: JSON.stringify({ _id: '$foo' }),
},
});
response = await request(options);
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
});
it_only_mongodb_version('>=5.2')('query aggregate with hint (rest)', async () => {
it_only_mongodb_version('>=7')('query aggregate with hint (rest)', async () => {
const object = new TestObject({ foo: 'bar' });
await object.save();
let options = Object.assign({}, masterKeyOptions, {