From a0af789c72a3cfee9ba1a13b0a861c5a70e50f5a Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 2 Jun 2017 23:23:17 -0500 Subject: [PATCH 01/12] Full Text Support --- spec/ParseQuery.spec.js | 74 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 5 ++ src/Adapters/Storage/Mongo/MongoTransform.js | 55 ++++++++++++++ src/Controllers/DatabaseController.js | 2 +- 4 files changed, 135 insertions(+), 1 deletion(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index b238e24e63..0327f6c775 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4,7 +4,10 @@ // Some new tests are added. 'use strict'; +const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); +const databaseURI = 'mongodb://localhost:27017/test'; const Parse = require('parse/node'); +const rp = require('request-promise'); describe('Parse.Query testing', () => { it("basic query", function(done) { @@ -2816,4 +2819,75 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $search', (done) => { + const adapter = new MongoStorageAdapter({ uri: databaseURI }); + reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter: adapter + }).then(() => { + return adapter.createIndex('TestObject', {subject:'text'}); + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests: [ + { + method: "POST", + body: { + subject: "Joe owns a dog" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "Dogs eat cats and dog eats pigeons too" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "Cats eat rats" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "Rats eat Joe" + }, + path: "/1/classes/TestObject" + } + ] + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then(() => { + const where = { + $text: { + $search: 'dogs' + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + console.log(resp.results[0]); + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 267e9f7a59..43bd861013 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -393,6 +393,11 @@ export class MongoStorageAdapter { performInitialization() { return Promise.resolve(); } + + createIndex(className, index) { + return this._adaptiveCollection(className) + .then(collection => collection._mongoCollection.createIndex(index)); + } } export default MongoStorageAdapter; diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index a9bdda3d40..e87ddef5e3 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -576,6 +576,61 @@ function transformConstraint(constraint, inArray) { answer[key] = constraint[key]; break; + case '$search': { + const s = constraint[key]; + if (typeof s !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: ${s}, should be string` + ); + } + answer[key] = s; + break; + } + case '$language': { + const s = constraint[key]; + if (typeof s !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: ${s}, should be string` + ); + } + answer[key] = s; + break; + } + case '$caseSensitive': { + const s = constraint[key]; + if (typeof s !== 'boolean') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: ${s}, should be boolean` + ); + } + answer[key] = s; + break; + } + case '$diacriticSensitive': { + const s = constraint[key]; + if (typeof s !== 'boolean') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: ${s}, should be boolean` + ); + } + answer[key] = s; + break; + } + case '$meta': { + const s = constraint[key]; + if (s !== 'textScore') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $meta: ${s}, should be textScore` + ); + } + answer[key] = s; + break; + } case '$nearSphere': var point = constraint[key]; answer[key] = [point.longitude, point.latitude]; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 40cbeddf0b..425a992949 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -42,7 +42,7 @@ const transformObjectACL = ({ ACL, ...result }) => { return result; } -const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; +const specialQuerykeys = ['$and', '$or', '$text', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; const isSpecialQueryKey = key => { return specialQuerykeys.indexOf(key) >= 0; From a6945d93a4cce0a2043713e4ecdefc01139c1323 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 3 Jun 2017 00:22:11 -0500 Subject: [PATCH 02/12] invalid input test --- spec/ParseQuery.spec.js | 230 ++++++++++++++++++++++++++++++++++------ 1 file changed, 195 insertions(+), 35 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 0327f6c775..2dc9b80b54 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2820,9 +2820,30 @@ describe('Parse.Query testing', () => { }, done.fail); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $search', (done) => { + const fullTextHelper = () => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); - reconfigureServer({ + const subjects = [ + 'coffee', + 'Coffee Shopping', + 'Baking a cake', + 'baking', + 'Café Con Leche', + 'Сырники', + 'coffee and cream', + 'Cafe con Leche', + ]; + const requests = []; + for (const i in subjects) { + const request = { + method: "POST", + body: { + subject: subjects[i] + }, + path: "/1/classes/TestObject" + }; + requests.push(request); + } + return reconfigureServer({ appId: 'test', restAPIKey: 'test', publicServerURL: 'http://localhost:8378/1', @@ -2833,36 +2854,7 @@ describe('Parse.Query testing', () => { return rp.post({ url: 'http://localhost:8378/1/batch', body: { - requests: [ - { - method: "POST", - body: { - subject: "Joe owns a dog" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "Dogs eat cats and dog eats pigeons too" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "Cats eat rats" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "Rats eat Joe" - }, - path: "/1/classes/TestObject" - } - ] + requests }, json: true, headers: { @@ -2870,10 +2862,36 @@ describe('Parse.Query testing', () => { 'X-Parse-REST-API-Key': 'test' } }); - }).then(() => { + }); + } + + it_exclude_dbs(['postgres'])('fullTextSearch: $search', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'coffee' + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(3); + done(); + }, done.fail); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $language', (done) => { + fullTextHelper().then(() => { const where = { $text: { - $search: 'dogs' + $search: 'leche', + $language: 'es' } }; return rp.post({ @@ -2885,9 +2903,151 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - console.log(resp.results[0]); expect(resp.results.length).toBe(2); done(); }, done.fail); }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $caseSensitive', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'Coffee', + $caseSensitive: true + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $diacriticSensitive', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'CAFÉ', + $diacriticSensitive: true + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $search, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: true + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $language, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'leche', + $language: true + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $caseSensitive, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'Coffee', + $caseSensitive: 'string' + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('fullTextSearch: $diacriticSensitive, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'CAFÉ', + $diacriticSensitive: 'string' + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); }); From acde6dc0396d06262fbbd82da1e7d1d9d350b52c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 3 Jun 2017 11:12:58 -0500 Subject: [PATCH 03/12] Support for sort --- spec/ParseQuery.spec.js | 25 ++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 40 ++++++++++++++----- src/Adapters/Storage/Mongo/MongoTransform.js | 11 ----- src/RestQuery.js | 4 +- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 2dc9b80b54..b0ca758cd9 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2886,6 +2886,31 @@ describe('Parse.Query testing', () => { }, done.fail); }); + it_exclude_dbs(['postgres'])('fullTextSearch: $search, sort', (done) => { + fullTextHelper().then(() => { + const where = { + $text: { + $search: 'coffee' + } + }; + const order = '$score'; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, order, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(3); + expect(resp.results[0].score).toBe(1); + expect(resp.results[1].score).toBe(0.75); + expect(resp.results[2].score).toBe(0.75); + done(); + }, done.fail); + }); + it_exclude_dbs(['postgres'])('fullTextSearch: $language', (done) => { fullTextHelper().then(() => { const where = { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 43bd861013..75225781d1 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -135,9 +135,13 @@ export class MongoStorageAdapter { this.database.close(false); } - _adaptiveCollection(name: string) { + _rawCollection(name: string) { return this.connect() - .then(() => this.database.collection(this._collectionPrefix + name)) + .then(() => this.database.collection(this._collectionPrefix + name)); + } + + _adaptiveCollection(name: string) { + return this._rawCollection(name) .then(rawCollection => new MongoCollection(rawCollection)); } @@ -340,15 +344,31 @@ export class MongoStorageAdapter { memo[transformKey(className, key, schema)] = 1; return memo; }, {}); + if (mongoWhere.$text) { + return new Promise((resolve, reject) => { + this._rawCollection(className).then((rawCollection) => { + rawCollection.find( + mongoWhere, + {score: {$meta: 'textScore'}} + ).sort(mongoSort).toArray(function(err, objects) { + if (err) { + reject(err); + } else { + resolve(objects.map(object => mongoObjectToParseObject(className, object, schema))); + } + }); + }); + }); + } return this._adaptiveCollection(className) - .then(collection => collection.find(mongoWhere, { - skip, - limit, - sort: mongoSort, - keys: mongoKeys, - maxTimeMS: this._maxTimeMS, - })) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) + .then(collection => collection.find(mongoWhere, { + skip, + limit, + sort: mongoSort, + keys: mongoKeys, + maxTimeMS: this._maxTimeMS, + })) + .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index e87ddef5e3..e79b3c590e 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -620,17 +620,6 @@ function transformConstraint(constraint, inArray) { answer[key] = s; break; } - case '$meta': { - const s = constraint[key]; - if (s !== 'textScore') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $meta: ${s}, should be textScore` - ); - } - answer[key] = s; - break; - } case '$nearSphere': var point = constraint[key]; answer[key] = [point.longitude, point.latitude]; diff --git a/src/RestQuery.js b/src/RestQuery.js index 37cbd518d6..c72eb3d5c0 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -94,7 +94,9 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl var fields = restOptions.order.split(','); this.findOptions.sort = fields.reduce((sortMap, field) => { field = field.trim(); - if (field[0] == '-') { + if (field == '$score') { + sortMap['score'] = {$meta: 'textScore'}; + } else if (field[0] == '-') { sortMap[field.slice(1)] = -1; } else { sortMap[field] = 1; From 22e58c7c1927d738798a185e7ff7494c2e0a7a4b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 3 Jun 2017 14:59:32 -0500 Subject: [PATCH 04/12] index exist test --- spec/ParseQuery.spec.js | 60 ++++++++++++++++++- src/Adapters/Storage/Mongo/MongoCollection.js | 5 ++ .../Storage/Mongo/MongoStorageAdapter.js | 24 +------- src/Controllers/DatabaseController.js | 4 +- src/RestQuery.js | 4 +- 5 files changed, 71 insertions(+), 26 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index b0ca758cd9..25745af237 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2865,6 +2865,63 @@ describe('Parse.Query testing', () => { }); } + it_exclude_dbs(['postgres'])('fullTextSearch: $search, index not exist', (done) => { + const adapter = new MongoStorageAdapter({ uri: databaseURI }); + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter: adapter + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests: [ + { + method: "POST", + body: { + subject: "coffee is java" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "java is coffee" + }, + path: "/1/classes/TestObject" + } + ] + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then(() => { + const where = { + $text: { + $search: 'coffee' + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`Text Index should not exist: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); + done(); + }); + }); + it_exclude_dbs(['postgres'])('fullTextSearch: $search', (done) => { fullTextHelper().then(() => { const where = { @@ -2894,9 +2951,10 @@ describe('Parse.Query testing', () => { } }; const order = '$score'; + const keys = '$score'; return rp.post({ url: 'http://localhost:8378/1/classes/TestObject', - json: { where, order, '_method': 'GET' }, + json: { where, order, keys, '_method': 'GET' }, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'test' diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index bf21df5dac..eb1fdfc805 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -14,6 +14,11 @@ export default class MongoCollection { // 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 } = {}) { + // 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 }) .catch(error => { // Check for "no geoindex" error diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 75225781d1..f7f994f09a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -135,13 +135,9 @@ export class MongoStorageAdapter { this.database.close(false); } - _rawCollection(name: string) { - return this.connect() - .then(() => this.database.collection(this._collectionPrefix + name)); - } - _adaptiveCollection(name: string) { - return this._rawCollection(name) + return this.connect() + .then(() => this.database.collection(this._collectionPrefix + name)) .then(rawCollection => new MongoCollection(rawCollection)); } @@ -344,22 +340,6 @@ export class MongoStorageAdapter { memo[transformKey(className, key, schema)] = 1; return memo; }, {}); - if (mongoWhere.$text) { - return new Promise((resolve, reject) => { - this._rawCollection(className).then((rawCollection) => { - rawCollection.find( - mongoWhere, - {score: {$meta: 'textScore'}} - ).sort(mongoSort).toArray(function(err, objects) { - if (err) { - reject(err); - } else { - resolve(objects.map(object => mongoObjectToParseObject(className, object, schema))); - } - }); - }); - }); - } return this._adaptiveCollection(className) .then(collection => collection.find(mongoWhere, { skip, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 425a992949..1408ef4edc 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -843,7 +843,9 @@ DatabaseController.prototype.find = function(className, query, { .then(objects => objects.map(object => { object = untransformObjectACL(object); return filterSensitiveData(isMaster, aclGroup, className, object) - })); + })).catch((error) => { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error); + }); } } }); diff --git a/src/RestQuery.js b/src/RestQuery.js index c72eb3d5c0..e59e6f1b0d 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -94,8 +94,8 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl var fields = restOptions.order.split(','); this.findOptions.sort = fields.reduce((sortMap, field) => { field = field.trim(); - if (field == '$score') { - sortMap['score'] = {$meta: 'textScore'}; + if (field === '$score') { + sortMap.score = {$meta: 'textScore'}; } else if (field[0] == '-') { sortMap[field.slice(1)] = -1; } else { From 217344e2cdb0a992cc1199b96b0972da29a5d891 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 3 Jun 2017 15:04:01 -0500 Subject: [PATCH 05/12] clean up --- .../Storage/Mongo/MongoStorageAdapter.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index f7f994f09a..43bd861013 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -341,14 +341,14 @@ export class MongoStorageAdapter { return memo; }, {}); return this._adaptiveCollection(className) - .then(collection => collection.find(mongoWhere, { - skip, - limit, - sort: mongoSort, - keys: mongoKeys, - maxTimeMS: this._maxTimeMS, - })) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) + .then(collection => collection.find(mongoWhere, { + skip, + limit, + sort: mongoSort, + keys: mongoKeys, + maxTimeMS: this._maxTimeMS, + })) + .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't From 45f1c128703e003a030312ba08ff7f1b640e19e1 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 3 Jun 2017 15:06:39 -0500 Subject: [PATCH 06/12] better error messaging --- src/Adapters/Storage/Mongo/MongoTransform.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index e79b3c590e..016655763d 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -581,7 +581,7 @@ function transformConstraint(constraint, inArray) { if (typeof s !== 'string') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${s}, should be string` + `bad $text: ${key}, should be string` ); } answer[key] = s; @@ -592,7 +592,7 @@ function transformConstraint(constraint, inArray) { if (typeof s !== 'string') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${s}, should be string` + `bad $text: ${key}, should be string` ); } answer[key] = s; @@ -603,7 +603,7 @@ function transformConstraint(constraint, inArray) { if (typeof s !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${s}, should be boolean` + `bad $text: ${key}, should be boolean` ); } answer[key] = s; @@ -614,7 +614,7 @@ function transformConstraint(constraint, inArray) { if (typeof s !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${s}, should be boolean` + `bad $text: ${key}, should be boolean` ); } answer[key] = s; From 44fdf5b40264707efe91f828effe0ef469dbd32b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 8 Jun 2017 19:53:30 -0500 Subject: [PATCH 07/12] postgres support --- spec/ParseQuery.spec.js | 131 ++++++++++++------ src/Adapters/Storage/Mongo/MongoTransform.js | 56 ++++---- .../Postgres/PostgresStorageAdapter.js | 48 +++++++ src/Controllers/DatabaseController.js | 2 +- 4 files changed, 168 insertions(+), 69 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 25745af237..ed5187c896 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -5,7 +5,9 @@ 'use strict'; const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); -const databaseURI = 'mongodb://localhost:27017/test'; +const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter'); +const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; const Parse = require('parse/node'); const rp = require('request-promise'); @@ -2821,7 +2823,12 @@ describe('Parse.Query testing', () => { }); const fullTextHelper = () => { - const adapter = new MongoStorageAdapter({ uri: databaseURI }); + let databaseAdapter; + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + databaseAdapter = new PostgresStorageAdapter({ uri: postgresURI }); + } else { + databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); + } const subjects = [ 'coffee', 'Coffee Shopping', @@ -2847,9 +2854,12 @@ describe('Parse.Query testing', () => { appId: 'test', restAPIKey: 'test', publicServerURL: 'http://localhost:8378/1', - databaseAdapter: adapter + databaseAdapter }).then(() => { - return adapter.createIndex('TestObject', {subject:'text'}); + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + return Parse.Promise.as(); + } + return databaseAdapter.createIndex('TestObject', {subject: 'text'}); }).then(() => { return rp.post({ url: 'http://localhost:8378/1/batch', @@ -2866,12 +2876,11 @@ describe('Parse.Query testing', () => { } it_exclude_dbs(['postgres'])('fullTextSearch: $search, index not exist', (done) => { - const adapter = new MongoStorageAdapter({ uri: databaseURI }); return reconfigureServer({ appId: 'test', restAPIKey: 'test', publicServerURL: 'http://localhost:8378/1', - databaseAdapter: adapter + databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) }).then(() => { return rp.post({ url: 'http://localhost:8378/1/batch', @@ -2901,8 +2910,12 @@ describe('Parse.Query testing', () => { }); }).then(() => { const where = { - $text: { - $search: 'coffee' + subject: { + $text: { + $search: { + $term: 'coffee' + } + } } }; return rp.post({ @@ -2922,11 +2935,15 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $search', (done) => { + it('fullTextSearch: $search', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'coffee' + subject: { + $text: { + $search: { + $term: 'coffee' + } + } } }; return rp.post({ @@ -2943,11 +2960,15 @@ describe('Parse.Query testing', () => { }, done.fail); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $search, sort', (done) => { + it('fullTextSearch: $search, sort', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'coffee' + subject: { + $text: { + $search: { + $term: 'coffee' + } + } } }; const order = '$score'; @@ -2962,19 +2983,23 @@ describe('Parse.Query testing', () => { }); }).then((resp) => { expect(resp.results.length).toBe(3); - expect(resp.results[0].score).toBe(1); - expect(resp.results[1].score).toBe(0.75); - expect(resp.results[2].score).toBe(0.75); + expect(resp.results[0].score); + expect(resp.results[1].score); + expect(resp.results[2].score); done(); }, done.fail); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $language', (done) => { + it('fullTextSearch: $language', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'leche', - $language: 'es' + subject: { + $text: { + $search: { + $term: 'leche', + $language: 'spanish' + } + } } }; return rp.post({ @@ -2994,9 +3019,13 @@ describe('Parse.Query testing', () => { it_exclude_dbs(['postgres'])('fullTextSearch: $caseSensitive', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'Coffee', - $caseSensitive: true + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: true + } + } } }; return rp.post({ @@ -3016,9 +3045,13 @@ describe('Parse.Query testing', () => { it_exclude_dbs(['postgres'])('fullTextSearch: $diacriticSensitive', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'CAFÉ', - $diacriticSensitive: true + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: true + } + } } }; return rp.post({ @@ -3035,11 +3068,13 @@ describe('Parse.Query testing', () => { }, done.fail); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $search, invalid input', (done) => { + it('fullTextSearch: $search, invalid input', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: true + subject: { + $text: { + $search: true + } } }; return rp.post({ @@ -3059,12 +3094,16 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $language, invalid input', (done) => { + it('fullTextSearch: $language, invalid input', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'leche', - $language: true + subject: { + $text: { + $search: { + $term: 'leche', + $language: true + } + } } }; return rp.post({ @@ -3084,12 +3123,16 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $caseSensitive, invalid input', (done) => { + it('fullTextSearch: $caseSensitive, invalid input', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'Coffee', - $caseSensitive: 'string' + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: 'string' + } + } } }; return rp.post({ @@ -3109,12 +3152,16 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $diacriticSensitive, invalid input', (done) => { + it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { fullTextHelper().then(() => { const where = { - $text: { - $search: 'CAFÉ', - $diacriticSensitive: 'string' + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: 'string' + } + } } }; return rp.post({ diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 016655763d..532844d44b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -228,6 +228,9 @@ function transformQueryKeyValue(className, key, value, schema) { // Handle query constraints const transformedConstraint = transformConstraint(value, expectedTypeIsArray); if (transformedConstraint !== CannotTransform) { + if (transformedConstraint.$text) { + return {key: '$text', value: transformedConstraint.$text}; + } return {key, value: transformedConstraint}; } @@ -576,48 +579,49 @@ function transformConstraint(constraint, inArray) { answer[key] = constraint[key]; break; - case '$search': { - const s = constraint[key]; - if (typeof s !== 'string') { + case '$text': { + const search = constraint[key].$search; + /* eslint-disable */ + if (typeof search !== 'object') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${key}, should be string` + `bad $text: $search, should be object` ); } - answer[key] = s; - break; - } - case '$language': { - const s = constraint[key]; - if (typeof s !== 'string') { + if (!search.$term || typeof search.$term !== 'string') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${key}, should be string` + `bad $text: $term, should be string` ); + } else { + answer[key] = { + '$search': search.$term + } } - answer[key] = s; - break; - } - case '$caseSensitive': { - const s = constraint[key]; - if (typeof s !== 'boolean') { + if (search.$language && typeof search.$language !== 'string') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${key}, should be boolean` + `bad $text: $language, should be string` ); + } else if (search.$language) { + answer[key].$language = search.$language; } - answer[key] = s; - break; - } - case '$diacriticSensitive': { - const s = constraint[key]; - if (typeof s !== 'boolean') { + if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: ${key}, should be boolean` + `bad $text: $caseSensitive, should be boolean` ); + } else if (search.$caseSensitive) { + answer[key].$caseSensitive = search.$caseSensitive; + } + if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $diacriticSensitive, should be boolean` + ); + } else if (search.$diacriticSensitive) { + answer[key].$diacriticSensitive = search.$diacriticSensitive; } - answer[key] = s; break; } case '$nearSphere': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 778b112865..d65de700c4 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -324,6 +324,51 @@ const buildWhereClause = ({ schema, query, index }) => { index += 1; } + if (fieldValue.$text) { + const search = fieldValue.$text.$search; + let language = 'english'; + if (typeof search !== 'object') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $search, should be object` + ); + } + if (!search.$term || typeof search.$term !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $term, should be string` + ); + } + if (search.$language && typeof search.$language !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $language, should be string` + ); + } else if (search.$language) { + language = search.$language; + } + if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $caseSensitive, should be boolean` + ); + } else if (search.$caseSensitive) { + //Skip, Use Regex or LOWER() + } + if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $diacriticSensitive, should be boolean` + ); + } else if (search.$diacriticSensitive) { + // Skip Use unaccent extention with text search config + // https://gist.github.com/rach/9289959 + } + patterns.push(`to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + 2}, $${index + 3})`); + values.push(language, fieldName, language, search.$term); + index += 4; + } + if (fieldValue.$nearSphere) { const point = fieldValue.$nearSphere; const distance = fieldValue.$maxDistance; @@ -1084,6 +1129,9 @@ export class PostgresStorageAdapter { return key.length > 0; }); columns = keys.map((key, index) => { + if (key === '$score') { + return `ts_rank_cd(to_tsvector($${2}, $${3}:name), to_tsquery($${4}, $${5}), 32) as score`; + } return `$${index + values.length + 1}:name`; }).join(','); values = values.concat(keys); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 1408ef4edc..1dfcc1a3f8 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -42,7 +42,7 @@ const transformObjectACL = ({ ACL, ...result }) => { return result; } -const specialQuerykeys = ['$and', '$or', '$text', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; +const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; const isSpecialQueryKey = key => { return specialQuerykeys.indexOf(key) >= 0; From 5c905f5082affafcdd246c4c60b5782ce330cd7a Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 13 Jun 2017 12:02:21 -0500 Subject: [PATCH 08/12] error instructions for $diacritic and $case sensitivity --- spec/ParseQuery.spec.js | 342 +++++++++++------- .../Postgres/PostgresStorageAdapter.js | 13 +- 2 files changed, 225 insertions(+), 130 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index ed5187c896..1af5235361 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -11,7 +11,61 @@ const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_tes const Parse = require('parse/node'); const rp = require('request-promise'); +const fullTextHelper = () => { + let databaseAdapter; + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + databaseAdapter = new PostgresStorageAdapter({ uri: postgresURI }); + } else { + databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); + } + const subjects = [ + 'coffee', + 'Coffee Shopping', + 'Baking a cake', + 'baking', + 'Café Con Leche', + 'Сырники', + 'coffee and cream', + 'Cafe con Leche', + ]; + const requests = []; + for (const i in subjects) { + const request = { + method: "POST", + body: { + subject: subjects[i] + }, + path: "/1/classes/TestObject" + }; + requests.push(request); + } + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter + }).then(() => { + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + return Parse.Promise.as(); + } + return databaseAdapter.createIndex('TestObject', {subject: 'text'}); + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }); +} + describe('Parse.Query testing', () => { + /* it("basic query", function(done) { var baz = new TestObject({ foo: 'baz' }); var qux = new TestObject({ foo: 'qux' }); @@ -2821,99 +2875,71 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); +*/ - const fullTextHelper = () => { - let databaseAdapter; - if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { - databaseAdapter = new PostgresStorageAdapter({ uri: postgresURI }); - } else { - databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); - } - const subjects = [ - 'coffee', - 'Coffee Shopping', - 'Baking a cake', - 'baking', - 'Café Con Leche', - 'Сырники', - 'coffee and cream', - 'Cafe con Leche', - ]; - const requests = []; - for (const i in subjects) { - const request = { - method: "POST", - body: { - subject: subjects[i] - }, - path: "/1/classes/TestObject" + it('fullTextSearch: $search', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' + } + } + } }; - requests.push(request); - } - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter - }).then(() => { - if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { - return Parse.Promise.as(); - } - return databaseAdapter.createIndex('TestObject', {subject: 'text'}); - }).then(() => { return rp.post({ - url: 'http://localhost:8378/1/batch', - body: { - requests - }, - json: true, + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'test' } }); - }); - } + }).then((resp) => { + expect(resp.results.length).toBe(3); + done(); + }, done.fail); + }); - it_exclude_dbs(['postgres'])('fullTextSearch: $search, index not exist', (done) => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) - }).then(() => { - return rp.post({ - url: 'http://localhost:8378/1/batch', - body: { - requests: [ - { - method: "POST", - body: { - subject: "coffee is java" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "java is coffee" - }, - path: "/1/classes/TestObject" + it('fullTextSearch: $search, sort', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' } - ] - }, - json: true, + } + } + }; + const order = '$score'; + const keys = '$score'; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, order, keys, '_method': 'GET' }, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'test' } }); - }).then(() => { + }).then((resp) => { + expect(resp.results.length).toBe(3); + expect(resp.results[0].score); + expect(resp.results[1].score); + expect(resp.results[2].score); + done(); + }, done.fail); + }); + + it('fullTextSearch: $language', (done) => { + fullTextHelper().then(() => { const where = { subject: { $text: { $search: { - $term: 'coffee' + $term: 'leche', + $language: 'spanish' } } } @@ -2927,21 +2953,19 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - fail(`Text Index should not exist: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); + expect(resp.results.length).toBe(2); done(); - }); + }, done.fail); }); - it('fullTextSearch: $search', (done) => { + it('fullTextSearch: $diacriticSensitive', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { - $term: 'coffee' + $term: 'CAFÉ', + $diacriticSensitive: true } } } @@ -2955,49 +2979,45 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - expect(resp.results.length).toBe(3); + expect(resp.results.length).toBe(1); done(); }, done.fail); }); - it('fullTextSearch: $search, sort', (done) => { + it('fullTextSearch: $search, invalid input', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { - $search: { - $term: 'coffee' - } + $search: true } } }; - const order = '$score'; - const keys = '$score'; return rp.post({ url: 'http://localhost:8378/1/classes/TestObject', - json: { where, order, keys, '_method': 'GET' }, + json: { where, '_method': 'GET' }, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'test' } }); }).then((resp) => { - expect(resp.results.length).toBe(3); - expect(resp.results[0].score); - expect(resp.results[1].score); - expect(resp.results[2].score); + fail(`no request should succeed: ${JSON.stringify(resp)}`); done(); - }, done.fail); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); }); - it('fullTextSearch: $language', (done) => { + it('fullTextSearch: $language, invalid input', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { $term: 'leche', - $language: 'spanish' + $language: true } } } @@ -3011,19 +3031,22 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - expect(resp.results.length).toBe(2); + fail(`no request should succeed: ${JSON.stringify(resp)}`); done(); - }, done.fail); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $caseSensitive', (done) => { + it('fullTextSearch: $caseSensitive, invalid input', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { $term: 'Coffee', - $caseSensitive: true + $caseSensitive: 'string' } } } @@ -3037,19 +3060,22 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - expect(resp.results.length).toBe(1); + fail(`no request should succeed: ${JSON.stringify(resp)}`); done(); - }, done.fail); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); }); - it_exclude_dbs(['postgres'])('fullTextSearch: $diacriticSensitive', (done) => { + it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { $term: 'CAFÉ', - $diacriticSensitive: true + $diacriticSensitive: 'string' } } } @@ -3063,17 +3089,56 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - expect(resp.results.length).toBe(1); + fail(`no request should succeed: ${JSON.stringify(resp)}`); done(); - }, done.fail); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); }); +}); - it('fullTextSearch: $search, invalid input', (done) => { - fullTextHelper().then(() => { +describe_only_db('mongo')('Parse.Query testing', () => { + it('fullTextSearch: $search, index not exist', (done) => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests: [ + { + method: "POST", + body: { + subject: "coffee is java" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "java is coffee" + }, + path: "/1/classes/TestObject" + } + ] + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then(() => { const where = { subject: { $text: { - $search: true + $search: { + $term: 'coffee' + } } } }; @@ -3086,22 +3151,22 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); + fail(`Text Index should not exist: ${JSON.stringify(resp)}`); done(); }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); done(); }); }); - it('fullTextSearch: $language, invalid input', (done) => { + it('fullTextSearch: $diacriticSensitive - false', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { - $term: 'leche', - $language: true + $term: 'CAFÉ', + $diacriticSensitive: false } } } @@ -3115,22 +3180,19 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + expect(resp.results.length).toBe(2); done(); - }); + }, done.fail); }); - it('fullTextSearch: $caseSensitive, invalid input', (done) => { + it('fullTextSearch: $caseSensitive', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { $term: 'Coffee', - $caseSensitive: 'string' + $caseSensitive: true } } } @@ -3144,7 +3206,35 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); + expect(resp.results.length).toBe(1); + done(); + }, done.fail); + }); +}); + +describe_only_db('postgres')('Parse.Query testing', () => { + it('fullTextSearch: $diacriticSensitive - false', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: false + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`$diacriticSensitive should not exist: ${JSON.stringify(resp)}`); done(); }).catch((err) => { expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); @@ -3152,14 +3242,14 @@ describe('Parse.Query testing', () => { }); }); - it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { + it('fullTextSearch: $caseSensitive', (done) => { fullTextHelper().then(() => { const where = { subject: { $text: { $search: { - $term: 'CAFÉ', - $diacriticSensitive: 'string' + $term: 'Coffee', + $caseSensitive: true } } } @@ -3173,7 +3263,7 @@ describe('Parse.Query testing', () => { } }); }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); + fail(`$caseSensitive should not exist: ${JSON.stringify(resp)}`); done(); }).catch((err) => { expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index d65de700c4..a518d6e2f9 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -353,16 +353,21 @@ const buildWhereClause = ({ schema, query, index }) => { `bad $text: $caseSensitive, should be boolean` ); } else if (search.$caseSensitive) { - //Skip, Use Regex or LOWER() + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $caseSensitive not supported, please use $regex or create a separate lower case column.` + ); } if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') { throw new Parse.Error( Parse.Error.INVALID_JSON, `bad $text: $diacriticSensitive, should be boolean` ); - } else if (search.$diacriticSensitive) { - // Skip Use unaccent extention with text search config - // https://gist.github.com/rach/9289959 + } else if (search.$diacriticSensitive === false) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $diacriticSensitive - false not supported, install Postgres Unaccent Extention` + ); } patterns.push(`to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + 2}, $${index + 3})`); values.push(language, fieldName, language, search.$term); From 638f2248d7a9fa025ccdd68ab1218ac8e83d9e98 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 13 Jun 2017 12:04:55 -0500 Subject: [PATCH 09/12] nit --- spec/ParseQuery.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 1af5235361..faabd1a6ed 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -65,7 +65,6 @@ const fullTextHelper = () => { } describe('Parse.Query testing', () => { - /* it("basic query", function(done) { var baz = new TestObject({ foo: 'baz' }); var qux = new TestObject({ foo: 'qux' }); @@ -2875,7 +2874,6 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); -*/ it('fullTextSearch: $search', (done) => { fullTextHelper().then(() => { From 70e40725c1265680e87c019efb89ed77cb3cb3c1 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 13 Jun 2017 12:07:59 -0500 Subject: [PATCH 10/12] nit --- src/Adapters/Storage/Mongo/MongoTransform.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 532844d44b..6c1c6ec6dd 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -581,7 +581,6 @@ function transformConstraint(constraint, inArray) { case '$text': { const search = constraint[key].$search; - /* eslint-disable */ if (typeof search !== 'object') { throw new Parse.Error( Parse.Error.INVALID_JSON, From 91160b6d8833e7b6c5f470e8b126ea161d44cbbc Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 13 Jun 2017 14:58:43 -0500 Subject: [PATCH 11/12] nit --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a518d6e2f9..2cfb766e66 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -366,7 +366,7 @@ const buildWhereClause = ({ schema, query, index }) => { } else if (search.$diacriticSensitive === false) { throw new Parse.Error( Parse.Error.INVALID_JSON, - `bad $text: $diacriticSensitive - false not supported, install Postgres Unaccent Extention` + `bad $text: $diacriticSensitive - false not supported, install Postgres Unaccent Extension` ); } patterns.push(`to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + 2}, $${index + 3})`); From c54865f6169e2f14b3dee0497e5f449222d2b0df Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 13 Jun 2017 18:20:52 -0500 Subject: [PATCH 12/12] separate test for full text --- spec/ParseQuery.FullTextSearch.spec.js | 459 +++++++++++++++++++++++++ spec/ParseQuery.spec.js | 452 ------------------------ 2 files changed, 459 insertions(+), 452 deletions(-) create mode 100644 spec/ParseQuery.FullTextSearch.spec.js diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js new file mode 100644 index 0000000000..a633df4307 --- /dev/null +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -0,0 +1,459 @@ +'use strict'; + +const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); +const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter'); +const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const Parse = require('parse/node'); +const rp = require('request-promise'); +let databaseAdapter; + +const fullTextHelper = () => { + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + if (!databaseAdapter) { + databaseAdapter = new PostgresStorageAdapter({ uri: postgresURI }); + } + } else { + databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); + } + const subjects = [ + 'coffee', + 'Coffee Shopping', + 'Baking a cake', + 'baking', + 'Café Con Leche', + 'Сырники', + 'coffee and cream', + 'Cafe con Leche', + ]; + const requests = []; + for (const i in subjects) { + const request = { + method: "POST", + body: { + subject: subjects[i] + }, + path: "/1/classes/TestObject" + }; + requests.push(request); + } + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter + }).then(() => { + if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { + return Parse.Promise.as(); + } + return databaseAdapter.createIndex('TestObject', {subject: 'text'}); + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }); +} + +describe('Parse.Query Full Text Search testing', () => { + it('fullTextSearch: $search', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(3); + done(); + }, done.fail); + }); + + it('fullTextSearch: $search, sort', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' + } + } + } + }; + const order = '$score'; + const keys = '$score'; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, order, keys, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(3); + expect(resp.results[0].score); + expect(resp.results[1].score); + expect(resp.results[2].score); + done(); + }, done.fail); + }); + + it('fullTextSearch: $language', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'leche', + $language: 'spanish' + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); + + it('fullTextSearch: $diacriticSensitive', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: true + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); + }); + + it('fullTextSearch: $search, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: true + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('fullTextSearch: $language, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'leche', + $language: true + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('fullTextSearch: $caseSensitive, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: 'string' + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: 'string' + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); +}); + +describe_only_db('mongo')('Parse.Query Full Text Search testing', () => { + it('fullTextSearch: $search, index not exist', (done) => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) + }).then(() => { + return rp.post({ + url: 'http://localhost:8378/1/batch', + body: { + requests: [ + { + method: "POST", + body: { + subject: "coffee is java" + }, + path: "/1/classes/TestObject" + }, + { + method: "POST", + body: { + subject: "java is coffee" + }, + path: "/1/classes/TestObject" + } + ] + }, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`Text Index should not exist: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); + done(); + }); + }); + + it('fullTextSearch: $diacriticSensitive - false', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: false + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); + + it('fullTextSearch: $caseSensitive', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: true + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); + }); +}); + +describe_only_db('postgres')('Parse.Query Full Text Search testing', () => { + it('fullTextSearch: $diacriticSensitive - false', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: false + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`$diacriticSensitive - false should not supported: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('fullTextSearch: $caseSensitive', (done) => { + fullTextHelper().then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: true + } + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test' + } + }); + }).then((resp) => { + fail(`$caseSensitive should not supported: ${JSON.stringify(resp)}`); + done(); + }).catch((err) => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); +}); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 9d41571ace..7bdfa56b57 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -4,65 +4,7 @@ // Some new tests are added. 'use strict'; -const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); -const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; -const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter'); -const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; const Parse = require('parse/node'); -const rp = require('request-promise'); - -const fullTextHelper = () => { - let databaseAdapter; - if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { - databaseAdapter = new PostgresStorageAdapter({ uri: postgresURI }); - } else { - databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); - } - const subjects = [ - 'coffee', - 'Coffee Shopping', - 'Baking a cake', - 'baking', - 'Café Con Leche', - 'Сырники', - 'coffee and cream', - 'Cafe con Leche', - ]; - const requests = []; - for (const i in subjects) { - const request = { - method: "POST", - body: { - subject: subjects[i] - }, - path: "/1/classes/TestObject" - }; - requests.push(request); - } - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter - }).then(() => { - if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { - return Parse.Promise.as(); - } - return databaseAdapter.createIndex('TestObject', {subject: 'text'}); - }).then(() => { - return rp.post({ - url: 'http://localhost:8378/1/batch', - body: { - requests - }, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }); -} describe('Parse.Query testing', () => { it("basic query", function(done) { @@ -2907,398 +2849,4 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); - - it('fullTextSearch: $search', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(3); - done(); - }, done.fail); - }); - - it('fullTextSearch: $search, sort', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - const order = '$score'; - const keys = '$score'; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, order, keys, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(3); - expect(resp.results[0].score); - expect(resp.results[1].score); - expect(resp.results[2].score); - done(); - }, done.fail); - }); - - it('fullTextSearch: $language', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'leche', - $language: 'spanish' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); - - it('fullTextSearch: $diacriticSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(1); - done(); - }, done.fail); - }); - - it('fullTextSearch: $search, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: true - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); - - it('fullTextSearch: $language, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'leche', - $language: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); - - it('fullTextSearch: $caseSensitive, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: 'string' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); - - it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: 'string' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); -}); - -describe_only_db('mongo')('Parse.Query testing', () => { - it('fullTextSearch: $search, index not exist', (done) => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) - }).then(() => { - return rp.post({ - url: 'http://localhost:8378/1/batch', - body: { - requests: [ - { - method: "POST", - body: { - subject: "coffee is java" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "java is coffee" - }, - path: "/1/classes/TestObject" - } - ] - }, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`Text Index should not exist: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); - done(); - }); - }); - - it('fullTextSearch: $diacriticSensitive - false', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: false - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); - - it('fullTextSearch: $caseSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(1); - done(); - }, done.fail); - }); -}); - -describe_only_db('postgres')('Parse.Query testing', () => { - it('fullTextSearch: $diacriticSensitive - false', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: false - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`$diacriticSensitive should not exist: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); - - it('fullTextSearch: $caseSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`$caseSensitive should not exist: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); - }); });