From cdf8a056aa4192d717ad4dfdd45130ccbba6687b Mon Sep 17 00:00:00 2001 From: jeremypiednoel Date: Tue, 15 May 2018 14:17:12 -0400 Subject: [PATCH 1/7] Adding elemMatch and nor --- src/Adapters/Storage/Mongo/MongoTransform.js | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index a5cbe1f195..c69e782cc1 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -208,10 +208,10 @@ function transformQueryKeyValue(className, key, value, schema) { case '_wperm': case '_perishable_token': case '_email_verify_token': return {key, value} - case '$or': - return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; case '$and': - return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; + case '$or': + case '$nor': + return { key: key, value: value.map(subQuery => transformWhere(className, subQuery, schema)) }; case 'lastUsed': if (valueAsDate(value)) { return {key: '_last_used', value: valueAsDate(value)} @@ -749,7 +749,22 @@ function transformConstraint(constraint, field) { } answer[key] = s; break; - + case '$elemMatch': { + const match = constraint[key]; + if (typeof match !== 'object') { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad match: $elemMatch, should be object`); + } + answer[key] = _.mapValues(match, value => { + return (atom => { + if (Array.isArray(atom)) { + return value.map(transformer); + } else { + return transformer(atom); + } + })(value); + }); + break; + } case '$options': answer[key] = constraint[key]; break; From f68ae6e74ad934e0c729ea12c85dd8f4a1e9b311 Mon Sep 17 00:00:00 2001 From: jeremypiednoel Date: Tue, 15 May 2018 15:43:14 -0400 Subject: [PATCH 2/7] lint --- src/Adapters/Storage/Mongo/MongoTransform.js | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index c69e782cc1..593a79aea7 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -749,22 +749,22 @@ function transformConstraint(constraint, field) { } answer[key] = s; break; - case '$elemMatch': { - const match = constraint[key]; - if (typeof match !== 'object') { - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad match: $elemMatch, should be object`); - } - answer[key] = _.mapValues(match, value => { - return (atom => { - if (Array.isArray(atom)) { - return value.map(transformer); - } else { - return transformer(atom); - } - })(value); - }); - break; + case '$elemMatch': { + const match = constraint[key]; + if (typeof match !== 'object') { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad match: $elemMatch, should be object`); } + answer[key] = _.mapValues(match, value => { + return (atom => { + if (Array.isArray(atom)) { + return value.map(transformer); + } else { + return transformer(atom); + } + })(value); + }); + break; + } case '$options': answer[key] = constraint[key]; break; From 7172a155865f559261e2e6f7602b9532823f1258 Mon Sep 17 00:00:00 2001 From: jeremypiednoel Date: Tue, 15 May 2018 16:52:31 -0400 Subject: [PATCH 3/7] adding test --- spec/DatabaseController.spec.js | 39 +++++++++++++++++++- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- src/Controllers/DatabaseController.js | 25 ++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index 43e85ccf87..f65c39aa7f 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -37,15 +37,50 @@ describe('DatabaseController', function() { }); it('should reject invalid queries', (done) => { - expect(() => validateQuery({$or: {'a': 1}})).toThrow(); + const objectQuery = {'a': 1}; + expect(() => validateQuery({$or: objectQuery})).toThrow(); + expect(() => validateQuery({$nor: objectQuery})).toThrow(); + expect(() => validateQuery({$and: objectQuery})).toThrow(); + + const array = [1, 2, 3, 4]; + expect(() => validateQuery({ + $and: [ + { 'a' : { $elemMatch : array }}, + ] + })).toThrow(); + + expect(() => validateQuery({ + $nor: [ + { 'a' : { $elemMatch : 1 }}, + ] + })).toThrow(); + done(); }); it('should accept valid queries', (done) => { - expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow(); + const arrayQuery = [{'a': 1}, {'b': 2}]; + expect(() => validateQuery({$or: arrayQuery})).not.toThrow(); + expect(() => validateQuery({$nor: arrayQuery})).not.toThrow(); + expect(() => validateQuery({$and: arrayQuery})).not.toThrow(); + + const array = [1, 2, 3, 4]; + expect(() => validateQuery({ + $nor: [ + { 'a' : { $elemMatch : { $nin : array } }} + ] + })).not.toThrow(); + + expect(() => validateQuery({ + $and: [ + { 'a' : { $elemMatch : { $nin : array } }}, + { 'b' : { $elemMatch : { $all : array } }} + ] + })).not.toThrow(); done(); }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 593a79aea7..ba5262f5be 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -751,7 +751,7 @@ function transformConstraint(constraint, field) { break; case '$elemMatch': { const match = constraint[key]; - if (typeof match !== 'object') { + if (typeof match !== 'object' || Array.isArray(match)) { throw new Parse.Error(Parse.Error.INVALID_JSON, `bad match: $elemMatch, should be object`); } answer[key] = _.mapValues(match, value => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ba814fb0a5..776bd56ea5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -50,7 +50,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', '$nor', '_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; @@ -103,6 +103,14 @@ const validateQuery = (query: any): void => { } } + if (query.$nor) { + if (query.$nor instanceof Array) { + query.$nor.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $nor format - use an array value.'); + } + } + if (query.$and) { if (query.$and instanceof Array) { query.$and.forEach(validateQuery); @@ -112,13 +120,20 @@ const validateQuery = (query: any): void => { } Object.keys(query).forEach(key => { - if (query && query[key] && query[key].$regex) { - if (typeof query[key].$options === 'string') { - if (!query[key].$options.match(/^[imxs]+$/)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); + if (query && query[key]) { + if (query[key].$regex) { + if (typeof query[key].$options === 'string') { + if (!query[key].$options.match(/^[imxs]+$/)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); + } + } + } else if (query[key].$elemMatch) { + if (typeof query[key].$elemMatch !== 'object' || Array.isArray(query[key].$elemMatch)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `bad match: $elemMatch, should be object`); } } } + if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); } From 4bbcf8ddeabd5b6908d57c82b38874a12c756486 Mon Sep 17 00:00:00 2001 From: jeremypiednoel Date: Thu, 17 May 2018 12:33:43 -0400 Subject: [PATCH 4/7] adding edge test --- spec/ParseQuery.spec.js | 74 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 23d8ad4a8a..bb5a3fb98c 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -523,7 +523,7 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -545,7 +545,7 @@ describe('Parse.Query testing', () => { equal(results.results.length, 1); arrayContains(results.results, object); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -568,7 +568,7 @@ describe('Parse.Query testing', () => { arrayContains(results.results, object); arrayContains(results.results, object3); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -602,7 +602,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -636,7 +636,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -666,7 +666,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -702,7 +702,7 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -732,7 +732,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return require('request-promise').get({ + return rp.get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -754,6 +754,62 @@ describe('Parse.Query testing', () => { }); }); + it('containsExclusivelySome number array queries', (done) => { + + const objects = Array.from(Array(10).keys()).map((idx) => { + const obj = new Parse.Object('Object'); + obj.set('key', idx); + return obj; + }); + + const parent = new Parse.Object('Parent'); + const parent2 = new Parse.Object('Parent'); + const parent3 = new Parse.Object('Parent'); + + Parse.Object.saveAll(objects).then(() => { + // [0, 1, 2] + parent.set('objects', objects.slice(0, 3)); + + const shift = objects.shift(); + // [2, 0] + parent2.set('objects', [objects[1], shift]); + + // [1, 2, 3, 4] + parent3.set('objects', objects.slice(1, 4)); + + return Parse.Object.saveAll([parent, parent2, parent3]); + }).then(() => { + // [1, 2, 3, 4, 5, 6, 7, 8, 9] + const pointers = objects.map(object => object.toPointer()); + + // Return all Parent where all parent.objects are contained in objects + return rp.get({ + url: Parse.serverURL + "/classes/Parent", + json: { + where: { + $nor : [{ + objects : { + $elemMatch : { + $nin : pointers + } + } + }] + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((results) => { + expect(results.results[0].objectId).not.toBeUndefined(); + expect(results.results[0].objectId).toBe(parent3.id); + expect(results.results.length).toBe(1); + done(); + }).catch((error) => { console.log(error); }); + }) + + const BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" }); @@ -3523,7 +3579,7 @@ describe('Parse.Query testing', () => { __type: 'Pointer', } } - return require('request-promise').post({ + return rp.post({ url: Parse.serverURL + "/classes/Game", json: { where, "_method": "GET" }, headers: { From 5899956db10fc77ad6c8f6cf3f21a548525a6c23 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 17 May 2018 23:34:20 -0500 Subject: [PATCH 5/7] postgres support --- spec/DatabaseController.spec.js | 39 +---------- spec/ParseQuery.spec.js | 70 +++++++++++++------ src/Adapters/Storage/Mongo/MongoTransform.js | 26 +++---- .../Postgres/PostgresStorageAdapter.js | 14 ++++ src/Controllers/DatabaseController.js | 25 ++----- 5 files changed, 84 insertions(+), 90 deletions(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index f65c39aa7f..43e85ccf87 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -37,50 +37,15 @@ describe('DatabaseController', function() { }); it('should reject invalid queries', (done) => { - const objectQuery = {'a': 1}; - expect(() => validateQuery({$or: objectQuery})).toThrow(); - expect(() => validateQuery({$nor: objectQuery})).toThrow(); - expect(() => validateQuery({$and: objectQuery})).toThrow(); - - const array = [1, 2, 3, 4]; - expect(() => validateQuery({ - $and: [ - { 'a' : { $elemMatch : array }}, - ] - })).toThrow(); - - expect(() => validateQuery({ - $nor: [ - { 'a' : { $elemMatch : 1 }}, - ] - })).toThrow(); - + expect(() => validateQuery({$or: {'a': 1}})).toThrow(); done(); }); it('should accept valid queries', (done) => { - const arrayQuery = [{'a': 1}, {'b': 2}]; - expect(() => validateQuery({$or: arrayQuery})).not.toThrow(); - expect(() => validateQuery({$nor: arrayQuery})).not.toThrow(); - expect(() => validateQuery({$and: arrayQuery})).not.toThrow(); - - const array = [1, 2, 3, 4]; - expect(() => validateQuery({ - $nor: [ - { 'a' : { $elemMatch : { $nin : array } }} - ] - })).not.toThrow(); - - expect(() => validateQuery({ - $and: [ - { 'a' : { $elemMatch : { $nin : array } }}, - { 'b' : { $elemMatch : { $all : array } }} - ] - })).not.toThrow(); + expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow(); done(); }); - }); }); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index bb5a3fb98c..bea3b3593a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -523,7 +523,7 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -545,7 +545,7 @@ describe('Parse.Query testing', () => { equal(results.results.length, 1); arrayContains(results.results, object); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -568,7 +568,7 @@ describe('Parse.Query testing', () => { arrayContains(results.results, object); arrayContains(results.results, object3); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -602,7 +602,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -636,7 +636,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -666,7 +666,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -702,7 +702,7 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -732,7 +732,7 @@ describe('Parse.Query testing', () => { object.save().then(() => { equal(object.isNew(), false); - return rp.get({ + return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", json: { where: { @@ -754,8 +754,7 @@ describe('Parse.Query testing', () => { }); }); - it('containsExclusivelySome number array queries', (done) => { - + it('containedBy pointer array', (done) => { const objects = Array.from(Array(10).keys()).map((idx) => { const obj = new Parse.Object('Object'); obj.set('key', idx); @@ -787,13 +786,9 @@ describe('Parse.Query testing', () => { url: Parse.serverURL + "/classes/Parent", json: { where: { - $nor : [{ - objects : { - $elemMatch : { - $nin : pointers - } - } - }] + objects: { + $containedBy: pointers + } } }, headers: { @@ -806,9 +801,44 @@ describe('Parse.Query testing', () => { expect(results.results[0].objectId).toBe(parent3.id); expect(results.results.length).toBe(1); done(); - }).catch((error) => { console.log(error); }); - }) + }); + }); + + it('containedBy number array', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { numbers: { $containedBy: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }, + } + }); + const obj1 = new TestObject({ numbers: [0, 1, 2] }); + const obj2 = new TestObject({ numbers: [2, 0] }); + const obj3 = new TestObject({ numbers: [1, 2, 3, 4] }); + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + return rp.get(Parse.serverURL + "/classes/TestObject", options); + }).then((results) => { + expect(results.results[0].objectId).not.toBeUndefined(); + expect(results.results[0].objectId).toBe(obj3.id); + expect(results.results.length).toBe(1); + done(); + }); + }); + + it('containedBy invalid query', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { objects: { $containedBy: 1234 } }, + } + }); + const obj = new TestObject(); + obj.save().then(() => { + return rp.get(Parse.serverURL + "/classes/TestObject", options); + }).then(done.fail).catch((error) => { + equal(error.error.code, Parse.Error.INVALID_JSON); + equal(error.error.error, 'bad $containedBy: should be an array'); + done(); + }); + }); const BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" @@ -3579,7 +3609,7 @@ describe('Parse.Query testing', () => { __type: 'Pointer', } } - return rp.post({ + return require('request-promise').post({ url: Parse.serverURL + "/classes/Game", json: { where, "_method": "GET" }, headers: { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 4ba80aee0e..41870e9e24 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -290,6 +290,9 @@ function transformQueryKeyValue(className, key, value, schema) { if (transformedConstraint.$text) { return {key: '$text', value: transformedConstraint.$text}; } + if (transformedConstraint.$elemMatch) { + return { key: '$nor', value: [{ [key]: transformedConstraint }] }; + } return {key, value: transformedConstraint}; } @@ -796,20 +799,17 @@ function transformConstraint(constraint, field) { } answer[key] = s; break; - case '$elemMatch': { - const match = constraint[key]; - if (typeof match !== 'object' || Array.isArray(match)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad match: $elemMatch, should be object`); + case '$containedBy': { + const arr = constraint[key]; + if (!(arr instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $containedBy: should be an array` + ); } - answer[key] = _.mapValues(match, value => { - return (atom => { - if (Array.isArray(atom)) { - return value.map(transformer); - } else { - return transformer(atom); - } - })(value); - }); + answer.$elemMatch = { + $nin: arr.map(transformer) + }; break; } case '$options': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 2c08b4b375..ec98ade758 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -446,6 +446,20 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { index += 1; } + if (fieldValue.$containedBy) { + const arr = fieldValue.$containedBy; + if (!(arr instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $containedBy: should be an array` + ); + } + + patterns.push(`$${index}:name <@ $${index + 1}::jsonb`); + values.push(fieldName, JSON.stringify(arr)); + index += 2; + } + if (fieldValue.$text) { const search = fieldValue.$text.$search; let language = 'english'; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 776bd56ea5..ba814fb0a5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -50,7 +50,7 @@ const transformObjectACL = ({ ACL, ...result }) => { return result; } -const specialQuerykeys = ['$and', '$or', '$nor', '_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; @@ -103,14 +103,6 @@ const validateQuery = (query: any): void => { } } - if (query.$nor) { - if (query.$nor instanceof Array) { - query.$nor.forEach(validateQuery); - } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $nor format - use an array value.'); - } - } - if (query.$and) { if (query.$and instanceof Array) { query.$and.forEach(validateQuery); @@ -120,20 +112,13 @@ const validateQuery = (query: any): void => { } Object.keys(query).forEach(key => { - if (query && query[key]) { - if (query[key].$regex) { - if (typeof query[key].$options === 'string') { - if (!query[key].$options.match(/^[imxs]+$/)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); - } - } - } else if (query[key].$elemMatch) { - if (typeof query[key].$elemMatch !== 'object' || Array.isArray(query[key].$elemMatch)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `bad match: $elemMatch, should be object`); + if (query && query[key] && query[key].$regex) { + if (typeof query[key].$options === 'string') { + if (!query[key].$options.match(/^[imxs]+$/)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); } } } - if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); } From e64c9014d7e6f0cc6124580c5fc184ca3364b733 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 17 May 2018 23:39:08 -0500 Subject: [PATCH 6/7] clean up --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 41870e9e24..11b239af1b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -246,10 +246,10 @@ function transformQueryKeyValue(className, key, value, schema) { case '_wperm': case '_perishable_token': case '_email_verify_token': return {key, value} - case '$and': case '$or': - case '$nor': - return { key: key, value: value.map(subQuery => transformWhere(className, subQuery, schema)) }; + return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; + case '$and': + return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; case 'lastUsed': if (valueAsDate(value)) { return {key: '_last_used', value: valueAsDate(value)} From 2d5d208d8a11febf96c3960513ee9141d73f0f97 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 17 May 2018 23:53:12 -0500 Subject: [PATCH 7/7] empty test --- spec/ParseQuery.spec.js | 17 +++++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 1 + 2 files changed, 18 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index bea3b3593a..d3881d5e2e 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -824,6 +824,23 @@ describe('Parse.Query testing', () => { }); }); + it('containedBy empty array', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { numbers: { $containedBy: [] } }, + } + }); + const obj1 = new TestObject({ numbers: [0, 1, 2] }); + const obj2 = new TestObject({ numbers: [2, 0] }); + const obj3 = new TestObject({ numbers: [1, 2, 3, 4] }); + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + return rp.get(Parse.serverURL + "/classes/TestObject", options); + }).then((results) => { + expect(results.results.length).toBe(0); + done(); + }); + }); + it('containedBy invalid query', (done) => { const options = Object.assign({}, masterKeyOptions, { body: { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 11b239af1b..f1952ea5da 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -799,6 +799,7 @@ function transformConstraint(constraint, field) { } answer[key] = s; break; + case '$containedBy': { const arr = constraint[key]; if (!(arr instanceof Array)) {