diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79d820cbff..de9f8aecd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index eaea473a85..57924885ff 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index eda843a334..44436d4986 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index a913930c28..8c2e177724 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -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'); diff --git a/spec/ParseQuery.hint.spec.js b/spec/ParseQuery.hint.spec.js index cd8a01cbe9..0905eb7d32 100644 --- a/spec/ParseQuery.hint.spec.js +++ b/spec/ParseQuery.hint.spec.js @@ -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,29 +79,7 @@ describe_only_db('mongo')('Parse.Query hint', () => { }); }); - it_only_mongodb_version('<4.4')('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.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 () => { + it_only_mongodb_version('<7')('query aggregate with hint string', async () => { const object = new TestObject({ foo: 'bar' }); await object.save(); @@ -134,7 +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; + 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,27 +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 () => { - 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.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 () => { + it_only_mongodb_version('<7')('query aggregate with hint object', async () => { const object = new TestObject({ foo: 'bar' }); await object.save(); @@ -226,7 +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; + 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, {