diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 0dcc83639c..f85d57a4ef 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -314,6 +314,19 @@ describe('Parse.Query testing', () => { equal(results.length, 0); }); + it('query without limit respects default limit', async done => { + const baz = new TestObject({ foo: 'baz' }); + const qux = new TestObject({ foo: 'qux' }); + await reconfigureServer({ defaultLimit: 1 }); + Parse.Object.saveAll([baz, qux]).then(function () { + const query = new Parse.Query(TestObject); + query.find().then(function (results) { + equal(results.length, 1); + done(); + }); + }); + }); + it('query with limit', function (done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); @@ -327,6 +340,20 @@ describe('Parse.Query testing', () => { }); }); + it('query with limit overrides default limit', async done => { + const baz = new TestObject({ foo: 'baz' }); + const qux = new TestObject({ foo: 'qux' }); + await reconfigureServer({ defaultLimit: 2 }); + Parse.Object.saveAll([baz, qux]).then(function () { + const query = new Parse.Query(TestObject); + query.limit(1); + query.find().then(function (results) { + equal(results.length, 1); + done(); + }); + }); + }); + it('query with limit equal to maxlimit', async () => { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); diff --git a/spec/index.spec.js b/spec/index.spec.js index dd9be1792b..f81861cb5c 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -462,6 +462,13 @@ describe('server', () => { .then(done); }); + it('fails if default limit is negative', done => { + reconfigureServer({ defaultLimit: -1 }).catch(error => { + expect(error).toEqual('Default limit must be a value greater than 0.'); + done(); + }); + }); + it('fails if maxLimit is negative', done => { reconfigureServer({ maxLimit: -100 }).catch(error => { expect(error).toEqual('Max limit must be a value greater than 0.'); @@ -469,6 +476,13 @@ describe('server', () => { }); }); + it('fails if maxLimit is smaller than the default limit', done => { + reconfigureServer({ defaultLimit: 101, maxLimit: 100 }).catch(error => { + expect(error).toEqual('Max limit must be greater than the default limit.'); + done(); + }); + }); + it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }).catch(done); }); diff --git a/src/Config.js b/src/Config.js index 04834d3291..040033d55b 100644 --- a/src/Config.js +++ b/src/Config.js @@ -63,6 +63,7 @@ export class Config { revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, + defaultLimit, maxLimit, emailVerifyTokenValidityDuration, accountLockout, @@ -110,7 +111,8 @@ export class Config { } this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateMasterKeyIps(masterKeyIps); - this.validateMaxLimit(maxLimit); + this.validateDefaultLimit(defaultLimit); + this.validateMaxLimit(maxLimit, defaultLimit); this.validateAllowHeaders(allowHeaders); this.validateIdempotencyOptions(idempotencyOptions); this.validatePagesOptions(pages); @@ -453,10 +455,19 @@ export class Config { } } - static validateMaxLimit(maxLimit) { + static validateDefaultLimit(defaultLimit) { + if (defaultLimit <= 0) { + throw 'Default limit must be a value greater than 0.'; + } + } + + static validateMaxLimit(maxLimit, defaultLimit) { if (maxLimit <= 0) { throw 'Max limit must be a value greater than 0.'; } + if (maxLimit < defaultLimit) { + throw 'Max limit must be greater than the default limit.'; + } } static validateAllowHeaders(allowHeaders) { diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index 4d21f24143..58a3985f25 100644 --- a/src/GraphQL/helpers/objectsQueries.js +++ b/src/GraphQL/helpers/objectsQueries.js @@ -272,7 +272,7 @@ const calculateSkipAndLimit = (skipInput, first, after, last, before, maxLimit) } if ((skip || 0) >= before) { - // If the before index is less then the skip, no objects will be returned + // If the before index is less than the skip, no objects will be returned limit = 0; } else if ((!limit && limit !== 0) || (skip || 0) + limit > before) { // If there is no limit set, the limit is calculated. Or, if the limit (plus skip) is bigger than the before index, the new limit is set. diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index c9a316db36..0707275d7e 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -154,6 +154,11 @@ module.exports.ParseServerOptions = { required: true, default: 'mongodb://localhost:27017/parse', }, + defaultLimit: { + env: 'PARSE_SERVER_DEFAULT_LIMIT', + help: 'Default limit for the size of results set on queries, defaults to 100', + action: parsers.numberParser('defaultLimit'), + }, directAccess: { env: 'PARSE_SERVER_DIRECT_ACCESS', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index e8601bd4e5..b86a40aad1 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -31,6 +31,7 @@ * @property {Adapter} databaseAdapter Adapter module for the database * @property {DatabaseOptions} databaseOptions Options to pass to the database client * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. + * @property {Number} defaultLimit Default limit for the size of results set on queries, defaults to 100 * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending diff --git a/src/Options/index.js b/src/Options/index.js index c298bc78e2..78019e34fe 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -194,6 +194,8 @@ export interface ParseServerOptions { /* Session duration, in seconds, defaults to 1 year :DEFAULT: 31536000 */ sessionLength: ?number; + /* Default limit for the size of results set on queries, defaults to 100 */ + defaultLimit: ?number; /* Max value for limit option on queries, defaults to unlimited */ maxLimit: ?number; /* Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date. diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 7f3e0a84f3..b502d185e4 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -20,7 +20,7 @@ export class ClassesRouter extends PromiseRouter { handleFind(req) { const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); - const options = ClassesRouter.optionsFromBody(body); + const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit); if (req.config.maxLimit && body.limit > req.config.maxLimit) { // Silently replace the limit on the query with the max configured options.limit = Number(req.config.maxLimit); @@ -149,7 +149,7 @@ export class ClassesRouter extends PromiseRouter { return json; } - static optionsFromBody(body) { + static optionsFromBody(body, defaultLimit) { const allowConstraints = [ 'skip', 'limit', @@ -180,7 +180,7 @@ export class ClassesRouter extends PromiseRouter { if (body.limit || body.limit === 0) { options.limit = Number(body.limit); } else { - options.limit = Number(100); + options.limit = Number(defaultLimit || 100); } if (body.order) { options.order = String(body.order);