diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 6b43fe4418d..6dc19bc6f2a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -314,39 +314,57 @@ describe('Parse.Query testing', () => { equal(results.length, 0); }); - it('query with limit', function (done) { - const baz = new TestObject({ foo: 'baz' }); - const qux = new TestObject({ foo: 'qux' }); - 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 without limit respects default limit', async () => { + await reconfigureServer({ defaultLimit: 1 }); + const obj1 = new TestObject({ foo: 'baz' }); + const obj2 = new TestObject({ foo: 'qux' }); + await Parse.Object.saveAll([obj1, obj2]); + const query = new Parse.Query(TestObject); + const result = await query.find(); + expect(result.length).toBe(1); + }); + + it('query with limit', async () => { + const obj1 = new TestObject({ foo: 'baz' }); + const obj2 = new TestObject({ foo: 'qux' }); + await Parse.Object.saveAll([obj1, obj2]); + const query = new Parse.Query(TestObject); + query.limit(1); + const result = await query.find(); + expect(result.length).toBe(1); + }); + + it('query with limit overrides default limit', async () => { + await reconfigureServer({ defaultLimit: 2 }); + const obj1 = new TestObject({ foo: 'baz' }); + const obj2 = new TestObject({ foo: 'qux' }); + await Parse.Object.saveAll([obj1, obj2]); + const query = new Parse.Query(TestObject); + query.limit(1); + const result = await query.find(); + expect(result.length).toBe(1); }); it('query with limit equal to maxlimit', async () => { - const baz = new TestObject({ foo: 'baz' }); - const qux = new TestObject({ foo: 'qux' }); await reconfigureServer({ maxLimit: 1 }); - await Parse.Object.saveAll([baz, qux]); + const obj1 = new TestObject({ foo: 'baz' }); + const obj2 = new TestObject({ foo: 'qux' }); + await Parse.Object.saveAll([obj1, obj2]); const query = new Parse.Query(TestObject); query.limit(1); - const results = await query.find(); - equal(results.length, 1); + const result = await query.find(); + expect(result.length).toBe(1); }); it('query with limit exceeding maxlimit', async () => { - const baz = new TestObject({ foo: 'baz' }); - const qux = new TestObject({ foo: 'qux' }); await reconfigureServer({ maxLimit: 1 }); - await Parse.Object.saveAll([baz, qux]); + const obj1 = new TestObject({ foo: 'baz' }); + const obj2 = new TestObject({ foo: 'qux' }); + await Parse.Object.saveAll([obj1, obj2]); const query = new Parse.Query(TestObject); query.limit(2); - const results = await query.find(); - equal(results.length, 1); + const result = await query.find(); + expect(result.length).toBe(1); }); it('containedIn object array queries', function (done) { diff --git a/spec/index.spec.js b/spec/index.spec.js index dd9be1792b0..0693ae5065a 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -462,6 +462,26 @@ describe('server', () => { .then(done); }); + it('fails if default limit is negative', async () => { + await expectAsync(reconfigureServer({ defaultLimit: -1 })).toBeRejectedWith( + 'Default limit must be a value greater than 0.' + ); + }); + + it('fails if default limit is wrong type', async () => { + for (const value of ["invalid", {}, [], true]) { + await expectAsync(reconfigureServer({ defaultLimit: value})).toBeRejectedWith( + 'Default limit must be a number.' + ); + } + }); + + it('fails if default limit is zero', async () => { + await expectAsync(reconfigureServer({ defaultLimit: 0 })).toBeRejectedWith( + 'Default limit must be a value greater than 0.' + ); + }); + it('fails if maxLimit is negative', done => { reconfigureServer({ maxLimit: -100 }).catch(error => { expect(error).toEqual('Max limit must be a value greater than 0.'); diff --git a/src/Config.js b/src/Config.js index 04834d32917..69a326b192f 100644 --- a/src/Config.js +++ b/src/Config.js @@ -12,6 +12,7 @@ import { PagesOptions, SecurityOptions, SchemaOptions, + ParseServerOptions } from './Options/Definitions'; import { isBoolean, isString } from 'lodash'; @@ -63,6 +64,7 @@ export class Config { revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, + defaultLimit, maxLimit, emailVerifyTokenValidityDuration, accountLockout, @@ -110,6 +112,7 @@ export class Config { } this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateMasterKeyIps(masterKeyIps); + this.validateDefaultLimit(defaultLimit); this.validateMaxLimit(maxLimit); this.validateAllowHeaders(allowHeaders); this.validateIdempotencyOptions(idempotencyOptions); @@ -453,6 +456,18 @@ export class Config { } } + static validateDefaultLimit(defaultLimit) { + if (defaultLimit == null) { + defaultLimit = ParseServerOptions.defaultLimit.default + } + if (typeof defaultLimit !== 'number') { + throw 'Default limit must be a number.'; + } + if (defaultLimit <= 0) { + throw 'Default limit must be a value greater than 0.'; + } + } + static validateMaxLimit(maxLimit) { if (maxLimit <= 0) { throw 'Max limit must be a value greater than 0.'; diff --git a/src/GraphQL/helpers/objectsQueries.js b/src/GraphQL/helpers/objectsQueries.js index 4d21f24143f..58a3985f252 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 6f9bec050e4..d8e3f841dd1 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -155,6 +155,12 @@ module.exports.ParseServerOptions = { required: true, default: 'mongodb://localhost:27017/parse', }, + defaultLimit: { + env: 'PARSE_SERVER_DEFAULT_LIMIT', + help: 'Default value for limit option on queries, defaults to `100`.', + action: parsers.numberParser('defaultLimit'), + default: 100, + }, directAccess: { env: 'PARSE_SERVER_DIRECT_ACCESS', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index d774aa46685..cbd06ecd253 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -31,6 +31,7 @@ * @property {Adapter} databaseAdapter Adapter module for the database; any options that are not explicitly described here are passed directly to the database client. * @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 value for limit option 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 23d266cb510..2592e1e4415 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -194,6 +194,9 @@ export interface ParseServerOptions { /* Session duration, in seconds, defaults to 1 year :DEFAULT: 31536000 */ sessionLength: ?number; + /* Default value for limit option on queries, defaults to `100`. + :DEFAULT: 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/AudiencesRouter.js b/src/Routers/AudiencesRouter.js index 43fcfd71bc8..991350512a0 100644 --- a/src/Routers/AudiencesRouter.js +++ b/src/Routers/AudiencesRouter.js @@ -9,7 +9,7 @@ export class AudiencesRouter extends ClassesRouter { 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); return rest .find( diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 7f3e0a84f38..5dfba97ee00 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); } if (body.order) { options.order = String(body.order); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index e7cbbbaef36..fa3e3a621c2 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -11,7 +11,7 @@ export class InstallationsRouter extends ClassesRouter { 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); return rest .find( req.config,