diff --git a/integration/test/ParseQueryTest.js b/integration/test/ParseQueryTest.js index 3760ca3fc..70916d0dd 100644 --- a/integration/test/ParseQueryTest.js +++ b/integration/test/ParseQueryTest.js @@ -23,6 +23,7 @@ describe('Parse Query', () => { .then(() => { done() }, () => { done() }); }); + it('can do basic queries', (done) => { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); @@ -51,6 +52,119 @@ describe('Parse Query', () => { }).catch(done.fail); }); + it('can do query with count', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: true })); + } + await Parse.Object.saveAll(items); + + const query = new Parse.Query(TestObject); + query.withCount(true); + const {results,count} = await query.find(); + + assert(typeof count === 'number'); + assert.equal(results.length, 4); + assert.equal(count, 4); + for (let i = 0; i < 4; i++) { + assert.equal(results[i].className,'TestObject'); + } + }); + + it('can do query withCount set to false', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: true })); + } + await Parse.Object.saveAll(items); + + const query = new Parse.Query(TestObject); + query.withCount(false); + const results = await query.find(); + + assert.equal(results.length, 4); + for (let i = 0; i < 4; i++) { + assert.equal(results[i].className,'TestObject'); + } + }); + + it('can do query with count on empty collection', async () => { + const query = new Parse.Query(TestObject); + query.withCount(true); + const {results,count} = await query.find(); + + assert(typeof count == 'number'); + assert.equal(results.length, 0); + assert.equal(count, 0); + }); + + it('can do query with count and limit', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: 2})); + } + await Parse.Object.saveAll(items); + const query = new Parse.Query(TestObject); + query.withCount(true); + query.limit(2); + + const {results,count} = await query.find(); + + assert(typeof count == 'number'); + assert.equal(results.length, 2); + assert.equal(count, 4); + }); + + it('can do query withCount and skip', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: 2})); + } + await Parse.Object.saveAll(items); + const query = new Parse.Query(TestObject); + query.withCount(true); + query.skip(3); + + const {results,count} = await query.find(); + + assert(typeof count == 'number'); + assert.equal(results.length, 1); + assert.equal(count, 4); + }); + + it('can do query when withCount set without arguments', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: 2})); + } + await Parse.Object.saveAll(items); + const query = new Parse.Query(TestObject); + query.withCount(); + + const {results,count} = await query.find(); + + assert(typeof count == 'number'); + assert.equal(results.length, 4); + assert.equal(count, 4); + }); + + it('can do query when withCount undefined', async () => { + const items = []; + for (let i = 0; i < 4; i++) { + items.push(new TestObject({ countMe: 2})); + } + await Parse.Object.saveAll(items); + const query = new Parse.Query(TestObject); + let foo; + query.withCount(foo); + + const {results,count} = await query.find(); + + assert(typeof count == 'number'); + assert.equal(results.length, 4); + assert.equal(count, 4); + }); + it('can do containedIn queries with arrays', (done) => { const messageList = []; for (let i = 0; i < 4; i++) { diff --git a/package-lock.json b/package-lock.json index 593e9b6da..53a235841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5670,8 +5670,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5694,15 +5693,13 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5718,20 +5715,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5861,8 +5855,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5876,7 +5869,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5893,7 +5885,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5901,15 +5892,13 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5930,7 +5919,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6018,8 +6006,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6033,7 +6020,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6128,8 +6114,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6171,7 +6156,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6193,7 +6177,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6241,14 +6224,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -9561,7 +9542,6 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9571,8 +9551,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/src/ParseQuery.js b/src/ParseQuery.js index 0f6cff424..93eceefd0 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -224,6 +224,7 @@ class ParseQuery { _select: Array; _limit: number; _skip: number; + _count: boolean; _order: Array; _readPreference: string; _includeReadPreference: string; @@ -260,6 +261,7 @@ class ParseQuery { this._where = {}; this._include = []; this._exclude = []; + this._count = false; this._limit = -1; // negative limit is not sent in the server request this._skip = 0; this._readPreference = null; @@ -364,6 +366,12 @@ class ParseQuery { return handleOfflineSort(a, b, sorts); }); } + + let count // count total before applying limit/skip + if(params.count){ + count = results.length; // total count from response + } + if (params.skip) { if (params.skip >= results.length) { results = []; @@ -375,7 +383,13 @@ class ParseQuery { if (params.limit !== 0 && params.limit < results.length) { limit = params.limit; } + results = results.splice(0, limit); + + if(typeof count === 'number'){ + return {results, count}; + } + return results; } @@ -397,6 +411,9 @@ class ParseQuery { if (this._select) { params.keys = this._select.join(','); } + if (this._count) { + params.count = 1; + } if (this._limit >= 0) { params.limit = this._limit; } @@ -460,6 +477,10 @@ class ParseQuery { this._exclude = json.excludeKeys.split(","); } + if (json.count) { + this._count = json.count === 1; + } + if (json.limit) { this._limit = json.limit; } @@ -486,7 +507,7 @@ class ParseQuery { for (const key in json) { if (json.hasOwnProperty(key)) { - if (["where", "include", "keys", "limit", "skip", "order", "readPreference", "includeReadPreference", "subqueryReadPreference"].indexOf(key) === -1) { + if (["where", "include", "keys", "count", "limit", "skip", "order", "readPreference", "includeReadPreference", "subqueryReadPreference"].indexOf(key) === -1) { this._extraOptions[key] = json[key]; } } @@ -588,7 +609,8 @@ class ParseQuery { this.toJSON(), findOptions ).then((response) => { - return response.results.map((data) => { + + const results = response.results.map((data) => { // In cases of relations, the server may send back a className // on the top level of the payload const override = response.className || this.className; @@ -605,6 +627,14 @@ class ParseQuery { return ParseObject.fromJSON(data, !select); }); + + const count = response.count; + + if(typeof count === "number"){ + return {results, count}; + } else { + return results; + } }); } @@ -1468,6 +1498,21 @@ class ParseQuery { return this; } + /** + * Sets the flag to include with response the total number of objects satisfying this query, + * despite limits/skip. Might be useful for pagination. + * Note that result of this query will be wrapped as an object with + *`results`: holding {ParseObject} array and `count`: integer holding total number + * @param {boolean} b false - disable, true - enable. + * @return {Parse.Query} Returns the query, so you can chain this call. + */ + withCount(includeCount: boolean = true): ParseQuery { + if (typeof includeCount !== 'boolean') { + throw new Error('You can only set withCount to a boolean value'); + } + this._count = includeCount; + return this; + } /** * Includes nested Parse.Objects for the provided key. You can use dot * notation to specify which fields in the included object are also fetched. diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 20edcb5fb..0eed5193d 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -898,6 +898,24 @@ describe('ParseQuery', () => { }); }); + it('can set withCount flag in find query', () => { + const q = new ParseQuery('Item'); + expect(q.withCount.bind(q, 'string')).toThrow( + 'You can only set withCount to a boolean value' + ); + + q.withCount(true); + expect(q.toJSON()).toEqual({ + where: {}, + count: 1 + }); + q.withCount(false); + expect(q.toJSON()).toEqual({ + where: {} + }); + }); + + it('can generate queries that include full data for pointers', () => { const q = new ParseQuery('Item'); q.greaterThan('inStock', 0); @@ -1414,6 +1432,44 @@ describe('ParseQuery', () => { }); }); + it('can receive both count and objects from find() using withCount flag', (done) => { + CoreManager.setQueryController({ + aggregate() {}, + find(className, params, options) { + expect(className).toBe('Item'); + expect(params).toEqual({ + where: {}, + count: 1 + }); + expect(options).toEqual({ + useMasterKey: true, + sessionToken: '1234' + }); + return Promise.resolve({ + results:[ + { objectId: '1', name: 'Product 55' }, + { objectId: '2', name: 'Product 89' } ], + count: 2 + }); + } + }); + + const q = new ParseQuery('Item'); + q.withCount(true) + .find({ + useMasterKey: true, + sessionToken: '1234' + }) + .then((obj) => { + expect(obj.results).toBeDefined(); + expect(obj.results.length).toBe(2); + expect(obj.count).toBeDefined(); + expect(typeof obj.count).toBe('number'); + done(); + }); + }); + + it('can iterate over results with each()', (done) => { CoreManager.setQueryController({ aggregate() {}, @@ -1809,6 +1865,7 @@ describe('ParseQuery', () => { q.include('manufacturer'); q.select('inStock', 'lastPurchase'); q.limit(10); + q.withCount(true); q.ascending(['a', 'b', 'c']); q.skip(4); q.equalTo('size', 'medium'); @@ -1823,6 +1880,7 @@ describe('ParseQuery', () => { include: 'manufacturer', keys: 'inStock,lastPurchase', limit: 10, + count: 1, order: 'a,b,c', skip: 4, where: { @@ -2670,6 +2728,98 @@ describe('ParseQuery LocalDatastore', () => { expect(results.length).toEqual(2); }); + it('can query offline withCount, skip and limit', async () => { + const obj1 = { + className: 'Item', + objectId: 'objectId1', + password: 123, + number: 3, + string: 'a', + }; + + const obj2 = { + className: 'Item', + objectId: 'objectId2', + number: 1, + string: 'b', + }; + + const obj3 = { + className: 'Item', + objectId: 'objectId3', + number: 2, + string: 'c', + }; + + const objects = [obj1, obj2, obj3]; + mockLocalDatastore + ._serializeObjectsFromPinName + .mockImplementation(() => objects); + + mockLocalDatastore + .checkIfEnabled + .mockImplementation(() => true); + + let q = new ParseQuery('Item'); + q.skip(0); + q.withCount(true); + q.fromLocalDatastore(); + let result = await q.find(); + expect(result.results.length).toEqual(3); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.skip(1); + q.withCount(true); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(2); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.skip(3); + q.withCount(true); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(0); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.withCount(true); + q.skip(4); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(0); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.limit(1); + q.skip(2); + q.withCount(true); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(1); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.limit(1); + q.skip(1); + q.withCount(true); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(1); + expect(result.count).toEqual(3); + + q = new ParseQuery('Item'); + q.limit(2); + q.skip(1); + q.withCount(true); + q.fromLocalDatastore(); + result = await q.find(); + expect(result.results.length).toEqual(2); + expect(result.count).toEqual(3); + }); + it('can query offline select keys', async () => { const obj1 = { className: 'Item',