diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index da53ca6292..7692d00fd8 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -5,6 +5,7 @@ 'use strict'; const Parse = require('parse/node'); +const rp = require('request-promise'); describe('Parse.Query testing', () => { it("basic query", function(done) { @@ -3007,4 +3008,109 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); + + it('distinct query', function(done) { + const score1 = new TestObject({score: 10}); + const score2 = new TestObject({score: 10}); + const score3 = new TestObject({score: 10}); + const score4 = new TestObject({score: 20}); + Parse.Object.saveAll([score1, score2, score3, score4]).then(() => { + const distinct = 'score'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(2); + expect(response.results.indexOf(10) > -1).toBe(true); + expect(response.results.indexOf(20) > -1).toBe(true); + done(); + }).catch(done.fail); + }); + + it('distinct nested', (done) => { + const sender1 = { group: 'A' }; + const sender2 = { group: 'A' }; + const sender3 = { group: 'B' }; + const obj1 = new TestObject({ sender: sender1 }); + const obj2 = new TestObject({ sender: sender2 }); + const obj3 = new TestObject({ sender: sender3 }); + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + const distinct = 'sender.group'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(2); + expect(response.results.indexOf('A') > -1).toBe(true); + expect(response.results.indexOf('B') > -1).toBe(true); + done(); + }).catch(done.fail); + }); + + it('distinct class does not exist return empty', (done) => { + const distinct = 'unknown'; + rp.post({ + url: Parse.serverURL + "/classes/UnknownDistinct", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }).then((response) => { + expect(response.results.length).toBe(0); + done(); + }).catch(done.fail); + }); + + it('distinct field does not exist return empty', function(done) { + const score = new TestObject({score: 10}); + score.save().then(() => { + const distinct = 'unknown'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(0); + done(); + }).catch(done.fail); + }); + + it('distinct array', function(done) { + const size1 = new TestObject({size: ['S', 'M']}); + const size2 = new TestObject({size: ['M', 'L']}); + const size3 = new TestObject({size: ['S']}); + const size4 = new TestObject({size: ['S']}); + Parse.Object.saveAll([size1, size2, size3, size4]).then(() => { + const distinct = 'size'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(3); + expect(response.results.indexOf('S') > -1).toBe(true); + expect(response.results.indexOf('M') > -1).toBe(true); + expect(response.results.indexOf('L') > -1).toBe(true); + done(); + }).catch(done.fail); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index ad1b458d25..65b3745e03 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -54,6 +54,10 @@ export default class MongoCollection { return findOperation.toArray(); } + distinct(field, query) { + return this._mongoCollection.distinct(field, query); + } + count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) { const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 3142712a3a..1aa2c57159 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -405,6 +405,13 @@ export class MongoStorageAdapter { })); } + // Finds unique values. + distinct(className, schema, query, fieldName) { + schema = convertParseSchemaToMongoSchema(schema); + return this._adaptiveCollection(className) + .then(collection => collection.distinct(fieldName, transformWhere(className, query, schema))); + } + _parseReadPreference(readPreference) { if (readPreference) { switch (readPreference) { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index f2aec54788..a58309e26a 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1366,6 +1366,39 @@ export class PostgresStorageAdapter { }); } + // Finds unique values. + distinct(className, schema, query, fieldName) { + debug('distinct', className, query); + let field = fieldName; + let column = fieldName; + if (fieldName.indexOf('.') >= 0) { + field = transformDotFieldToComponents(fieldName).join('->'); + column = fieldName.split('.')[0]; + } + const isArrayField = schema.fields + && schema.fields[fieldName] + && schema.fields[fieldName].type === 'Array'; + const values = [field, column, className]; + const where = buildWhereClause({ schema, query, index: 4 }); + values.push(...where.values); + + const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + let qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; + if (isArrayField) { + qs = `SELECT distinct jsonb_array_elements($1:raw) as $2:raw FROM $3:name ${wherePattern}`; + } + debug(qs, values); + return this._client.any(qs, values) + .catch(() => []) + .then((results) => { + if (fieldName.indexOf('.') === -1) { + return results.map(object => object[field]); + } + const child = fieldName.split('.')[1]; + return results.map(object => object[column][child]); + }); + } + performInitialization({ VolatileClassesSchemas }) { debug('performInitialization'); const promises = VolatileClassesSchemas.map((schema) => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c1cdfdadca..9acce14ff3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -782,6 +782,7 @@ DatabaseController.prototype.find = function(className, query, { limit, acl, sort = {}, + distinct, count, keys, op, @@ -853,6 +854,12 @@ DatabaseController.prototype.find = function(className, query, { } else { return this.adapter.count(className, schema, query, readPreference); } + } else if (distinct) { + if (!classExists) { + return []; + } else { + return this.adapter.distinct(className, schema, query, distinct); + } } else { if (!classExists) { return []; diff --git a/src/RestQuery.js b/src/RestQuery.js index 832149b145..5be07e1f99 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -86,6 +86,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl case 'count': this.doCount = true; break; + case 'distinct': case 'skip': case 'limit': case 'readPreference': diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 5793445a60..781edb98d1 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -103,7 +103,7 @@ export class ClassesRouter extends PromiseRouter { static optionsFromBody(body) { const allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', - 'include', 'redirectClassNameForKey', 'where']; + 'include', 'redirectClassNameForKey', 'where', 'distinct']; for (const key of Object.keys(body)) { if (allowConstraints.indexOf(key) === -1) { @@ -131,6 +131,9 @@ export class ClassesRouter extends PromiseRouter { if (body.include) { options.include = String(body.include); } + if (body.distinct) { + options.distinct = String(body.distinct); + } return options; }