From b6298feaa7124608a843b988164a4372b9900dc0 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 21 Jun 2017 17:18:10 -0300 Subject: [PATCH] Read preference option per query (#3865) --- spec/ReadPreferenceOption.spec.js | 748 ++++++++++++++++++ src/Adapters/Storage/Mongo/MongoCollection.js | 14 +- .../Storage/Mongo/MongoStorageAdapter.js | 34 +- src/Controllers/DatabaseController.js | 7 +- src/RestQuery.js | 29 + src/triggers.js | 12 + 6 files changed, 832 insertions(+), 12 deletions(-) create mode 100644 spec/ReadPreferenceOption.spec.js diff --git a/spec/ReadPreferenceOption.spec.js b/spec/ReadPreferenceOption.spec.js new file mode 100644 index 00000000..fbb8a052 --- /dev/null +++ b/spec/ReadPreferenceOption.spec.js @@ -0,0 +1,748 @@ +'use strict' + +const Parse = require('parse/node'); +const ReadPreference = require('mongodb').ReadPreference; +const rp = require('request-promise'); +const Config = require("../src/Config"); + +describe_only_db('mongo')('Read preference option', () => { + it('should find in primary by default', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.PRIMARY); + + done(); + }); + }); + }); + + it('should change read preference in the beforeFind trigger', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference in the beforeFind trigger even changing query', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.query.equalTo('boolKey', true); + req.readPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(true); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference in the beforeFind trigger even returning query', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + + const otherQuery = new Parse.Query('MyObject'); + otherQuery.equalTo('boolKey', true); + return otherQuery; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(true); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference in the beforeFind trigger even returning promise', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + + const otherQuery = new Parse.Query('MyObject'); + otherQuery.equalTo('boolKey', true); + return Promise.resolve(otherQuery); + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(true); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference to PRIMARY_PREFERRED', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'PRIMARY_PREFERRED'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.PRIMARY_PREFERRED); + + done(); + }); + }); + }); + + it('should change read preference to SECONDARY_PREFERRED', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY_PREFERRED'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY_PREFERRED); + + done(); + }); + }); + }); + + it('should change read preference to NEAREST', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'NEAREST'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.NEAREST); + + done(); + }); + }); + }); + + it('should change read preference for GET', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject'); + + query.get(obj0.id).then((result) => { + expect(result.get('boolKey')).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference for GET using API', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + }); + + rp({ + method: 'GET', + uri: 'http://localhost:8378/1/classes/MyObject/' + obj0.id, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + json: true, + }).then(body => { + expect(body.boolKey).toBe(false); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change read preference for count', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject'); + obj1.set('boolKey', true); + + Parse.Object.saveAll([obj0, obj1]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject', (req) => { + req.readPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject'); + query.equalTo('boolKey', false); + + query.count().then((result) => { + expect(result).toBe(1); + + let myObjectReadPreference = null; + databaseAdapter.database.serverConfig.command.calls.all().forEach((call) => { + myObjectReadPreference = call.args[2].readPreference.preference; + }); + + expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should find includes in primary by default', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject2'); + query.equalTo('boolKey', false); + query.include('myObject1'); + query.include('myObject1.myObject0'); + + query.find().then((results) => { + expect(results.length).toBe(1); + const firstResult = results[0]; + expect(firstResult.get('boolKey')).toBe(false); + expect(firstResult.get('myObject1').get('boolKey')).toBe(true); + expect(firstResult.get('myObject1').get('myObject0').get('boolKey')).toBe(false); + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.PRIMARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.PRIMARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change includes read preference', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY_PREFERRED'; + req.includeReadPreference = 'SECONDARY'; + }); + + const query = new Parse.Query('MyObject2'); + query.equalTo('boolKey', false); + query.include('myObject1'); + query.include('myObject1.myObject0'); + + query.find().then((results) => { + expect(results.length).toBe(1); + const firstResult = results[0]; + expect(firstResult.get('boolKey')).toBe(false); + expect(firstResult.get('myObject1').get('boolKey')).toBe(true); + expect(firstResult.get('myObject1').get('myObject0').get('boolKey')).toBe(false); + + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + + done(); + }); + }); + }); + + it('should find subqueries in primary by default', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY'; + }); + + const query0 = new Parse.Query('MyObject0'); + query0.equalTo('boolKey', false); + + const query1 = new Parse.Query('MyObject1'); + query1.matchesQuery('myObject0', query0); + + const query2 = new Parse.Query('MyObject2'); + query2.matchesQuery('myObject1', query1); + + query2.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.PRIMARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.PRIMARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY); + + done(); + }); + }); + }); + + it('should change subqueries read preference when using matchesQuery', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY_PREFERRED'; + req.subqueryReadPreference = 'SECONDARY'; + }); + + const query0 = new Parse.Query('MyObject0'); + query0.equalTo('boolKey', false); + + const query1 = new Parse.Query('MyObject1'); + query1.matchesQuery('myObject0', query0); + + const query2 = new Parse.Query('MyObject2'); + query2.matchesQuery('myObject1', query1); + + query2.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + + done(); + }); + }); + }); + + it('should change subqueries read preference when using doesNotMatchQuery', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY_PREFERRED'; + req.subqueryReadPreference = 'SECONDARY'; + }); + + const query0 = new Parse.Query('MyObject0'); + query0.equalTo('boolKey', false); + + const query1 = new Parse.Query('MyObject1'); + query1.doesNotMatchQuery('myObject0', query0); + + const query2 = new Parse.Query('MyObject2'); + query2.doesNotMatchQuery('myObject1', query1); + + query2.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + + done(); + }); + }); + }); + + it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', (done) => { + const databaseAdapter = (new Config(Parse.applicationId)).database.adapter; + + const obj0 = new Parse.Object('MyObject0'); + obj0.set('boolKey', false); + const obj1 = new Parse.Object('MyObject1'); + obj1.set('boolKey', true); + obj1.set('myObject0', obj0); + const obj2 = new Parse.Object('MyObject2'); + obj2.set('boolKey', false); + obj2.set('myObject1', obj1); + + Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { + spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); + + Parse.Cloud.beforeFind('MyObject2', (req) => { + req.readPreference = 'SECONDARY_PREFERRED'; + req.subqueryReadPreference = 'SECONDARY'; + }); + + const query0 = new Parse.Query('MyObject0'); + query0.equalTo('boolKey', false); + + const query1 = new Parse.Query('MyObject1'); + query1.equalTo('boolKey', true); + + const query2 = new Parse.Query('MyObject2'); + query2.matchesKeyInQuery('boolKey', 'boolKey', query0); + query2.doesNotMatchKeyInQuery('boolKey', 'boolKey', query1); + + query2.find().then((results) => { + expect(results.length).toBe(1); + expect(results[0].get('boolKey')).toBe(false); + + let myObjectReadPreference0 = null; + let myObjectReadPreference1 = null; + let myObjectReadPreference2 = null; + databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); + expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + + done(); + }); + }); + }); +}); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index eb1fdfc8..09ab7bac 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -13,13 +13,13 @@ export default class MongoCollection { // none, then build the geoindex. // This could be improved a lot but it's not clear if that's a good // idea. Or even if this behavior is a good idea. - find(query, { skip, limit, sort, keys, maxTimeMS } = {}) { + find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { // Support for Full Text Search - $text if(keys && keys.$score) { delete keys.$score; keys.score = {$meta: 'textScore'}; } - return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS }) + return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference }) .catch(error => { // Check for "no geoindex" error if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { @@ -35,13 +35,13 @@ export default class MongoCollection { index[key] = '2d'; return this._mongoCollection.createIndex(index) // Retry, but just once. - .then(() => this._rawFind(query, { skip, limit, sort, keys, maxTimeMS })); + .then(() => this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference })); }); } - _rawFind(query, { skip, limit, sort, keys, maxTimeMS } = {}) { + _rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { let findOperation = this._mongoCollection - .find(query, { skip, limit, sort }) + .find(query, { skip, limit, sort, readPreference }) if (keys) { findOperation = findOperation.project(keys); @@ -54,8 +54,8 @@ export default class MongoCollection { return findOperation.toArray(); } - count(query, { skip, limit, sort, maxTimeMS } = {}) { - const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS }); + count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) { + const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference }); return countOperation; } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 0995d643..ec8267a9 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -17,6 +17,7 @@ import defaults from '../../../defaults'; const mongodb = require('mongodb'); const MongoClient = mongodb.MongoClient; +const ReadPreference = mongodb.ReadPreference; const MongoSchemaCollectionName = '_SCHEMA'; @@ -332,7 +333,7 @@ export class MongoStorageAdapter { } // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. - find(className, schema, query, { skip, limit, sort, keys }) { + find(className, schema, query, { skip, limit, sort, keys, readPreference }) { schema = convertParseSchemaToMongoSchema(schema); const mongoWhere = transformWhere(className, query, schema); const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema)); @@ -340,6 +341,7 @@ export class MongoStorageAdapter { memo[transformKey(className, key, schema)] = 1; return memo; }, {}); + readPreference = this._parseReadPreference(readPreference); return this._adaptiveCollection(className) .then(collection => collection.find(mongoWhere, { skip, @@ -347,6 +349,7 @@ export class MongoStorageAdapter { sort: mongoSort, keys: mongoKeys, maxTimeMS: this._maxTimeMS, + readPreference, })) .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) } @@ -382,14 +385,41 @@ export class MongoStorageAdapter { } // Executes a count. - count(className, schema, query) { + count(className, schema, query, readPreference) { schema = convertParseSchemaToMongoSchema(schema); + readPreference = this._parseReadPreference(readPreference); return this._adaptiveCollection(className) .then(collection => collection.count(transformWhere(className, query, schema), { maxTimeMS: this._maxTimeMS, + readPreference, })); } + _parseReadPreference(readPreference) { + if (readPreference) { + switch (readPreference) { + case 'PRIMARY': + readPreference = ReadPreference.PRIMARY; + break; + case 'PRIMARY_PREFERRED': + readPreference = ReadPreference.PRIMARY_PREFERRED; + break; + case 'SECONDARY': + readPreference = ReadPreference.SECONDARY; + break; + case 'SECONDARY_PREFERRED': + readPreference = ReadPreference.SECONDARY_PREFERRED; + break; + case 'NEAREST': + readPreference = ReadPreference.NEAREST; + break; + default: + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Not supported read preference.'); + } + } + return readPreference; + } + performInitialization() { return Promise.resolve(); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 1f445467..bd06f4a7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -770,7 +770,8 @@ DatabaseController.prototype.find = function(className, query, { sort = {}, count, keys, - op + op, + readPreference } = {}) { const isMaster = acl === undefined; const aclGroup = acl || []; @@ -836,13 +837,13 @@ DatabaseController.prototype.find = function(className, query, { if (!classExists) { return 0; } else { - return this.adapter.count(className, schema, query); + return this.adapter.count(className, schema, query, readPreference); } } else { if (!classExists) { return []; } else { - return this.adapter.find(className, schema, query, { skip, limit, sort, keys }) + return this.adapter.find(className, schema, query, { skip, limit, sort, keys, readPreference }) .then(objects => objects.map(object => { object = untransformObjectACL(object); return filterSensitiveData(isMaster, aclGroup, className, object) diff --git a/src/RestQuery.js b/src/RestQuery.js index 5acb19d8..27e59c03 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -88,6 +88,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl break; case 'skip': case 'limit': + case 'readPreference': this.findOptions[option] = restOptions[option]; break; case 'order': @@ -128,6 +129,9 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl this.redirectKey = restOptions.redirectClassNameForKey; this.redirectClassName = null; break; + case 'includeReadPreference': + case 'subqueryReadPreference': + break; default: throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad option: ' + option); @@ -260,6 +264,11 @@ RestQuery.prototype.replaceInQuery = function() { redirectClassNameForKey: inQueryValue.redirectClassNameForKey }; + if (this.restOptions.subqueryReadPreference) { + additionalOptions.readPreference = this.restOptions.subqueryReadPreference; + additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference; + } + var subquery = new RestQuery( this.config, this.auth, inQueryValue.className, inQueryValue.where, additionalOptions); @@ -308,6 +317,11 @@ RestQuery.prototype.replaceNotInQuery = function() { redirectClassNameForKey: notInQueryValue.redirectClassNameForKey }; + if (this.restOptions.subqueryReadPreference) { + additionalOptions.readPreference = this.restOptions.subqueryReadPreference; + additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference; + } + var subquery = new RestQuery( this.config, this.auth, notInQueryValue.className, notInQueryValue.where, additionalOptions); @@ -358,6 +372,11 @@ RestQuery.prototype.replaceSelect = function() { redirectClassNameForKey: selectValue.query.redirectClassNameForKey }; + if (this.restOptions.subqueryReadPreference) { + additionalOptions.readPreference = this.restOptions.subqueryReadPreference; + additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference; + } + var subquery = new RestQuery( this.config, this.auth, selectValue.query.className, selectValue.query.where, additionalOptions); @@ -406,6 +425,11 @@ RestQuery.prototype.replaceDontSelect = function() { redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey }; + if (this.restOptions.subqueryReadPreference) { + additionalOptions.readPreference = this.restOptions.subqueryReadPreference; + additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference; + } + var subquery = new RestQuery( this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where, additionalOptions); @@ -605,6 +629,11 @@ function includePath(config, auth, response, path, restOptions = {}) { } } + if (restOptions.includeReadPreference) { + includeRestOptions.readPreference = restOptions.includeReadPreference; + includeRestOptions.includeReadPreference = restOptions.includeReadPreference; + } + const queryPromises = Object.keys(pointersHash).map((className) => { const where = {'objectId': {'$in': Array.from(pointersHash[className])}}; var query = new RestQuery(config, auth, className, where, includeRestOptions); diff --git a/src/triggers.js b/src/triggers.js index da1b729b..411c2c97 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -343,6 +343,18 @@ export function maybeRunQueryTrigger(triggerType, className, restWhere, restOpti restOptions = restOptions || {}; restOptions.keys = jsonQuery.keys; } + if (requestObject.readPreference) { + restOptions = restOptions || {}; + restOptions.readPreference = requestObject.readPreference; + } + if (requestObject.includeReadPreference) { + restOptions = restOptions || {}; + restOptions.includeReadPreference = requestObject.includeReadPreference; + } + if (requestObject.subqueryReadPreference) { + restOptions = restOptions || {}; + restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; + } return { restWhere, restOptions