diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index d3881d5e2e..1a755d5a75 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2567,6 +2567,63 @@ describe('Parse.Query testing', () => { }); }); + it('$nor valid query', (done) => { + const objects = Array.from(Array(10).keys()).map((rating) => { + return new TestObject({ 'rating': rating }); + }); + + const highValue = 5; + const lowValue = 3; + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { + $nor: [ + { rating : { $gt : highValue } }, + { rating : { $lte : lowValue } }, + ] + }, + } + }); + + Parse.Object.saveAll(objects).then(() => { + return rp.get(Parse.serverURL + "/classes/TestObject", options); + }).then((results) => { + expect(results.results.length).toBe(highValue - lowValue); + expect(results.results.every(res => res.rating > lowValue && res.rating <= highValue)).toBe(true); + done(); + }); + }); + + it('$nor invalid query - empty array', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { $nor: [] }, + } + }); + 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_QUERY); + done(); + }); + }); + + it('$nor invalid query - wrong type', (done) => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { $nor: 1337 }, + } + }); + 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_QUERY); + done(); + }); + }); + it("dontSelect query", function(done) { const RestaurantObject = Parse.Object.extend("Restaurant"); const PersonObject = Parse.Object.extend("Person"); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index f1952ea5da..c60db85d89 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -247,9 +247,9 @@ function transformQueryKeyValue(className, key, value, schema) { 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 '$nor': + return {key: key, value: value.map(subQuery => transformWhere(className, subQuery, schema))}; case 'lastUsed': if (valueAsDate(value)) { return {key: '_last_used', value: valueAsDate(value)} diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index ec98ade758..cd8759b92d 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -306,7 +306,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { patterns.push(`$${index}:name = $${index + 1}`); values.push(fieldName, fieldValue); index += 2; - } else if (fieldName === '$or' || fieldName === '$and') { + } else if (['$or', '$nor', '$and'].includes(fieldName)) { const clauses = []; const clauseValues = []; fieldValue.forEach((subQuery) => { @@ -317,8 +317,11 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { index += clause.values.length; } }); - const orOrAnd = fieldName === '$or' ? ' OR ' : ' AND '; - patterns.push(`(${clauses.join(orOrAnd)})`); + + const orOrAnd = fieldName === '$and' ? ' AND ' : ' OR '; + const not = fieldName === '$nor' ? ' NOT ' : ''; + + patterns.push(`${not}(${clauses.join(orOrAnd)})`); values.push(...clauseValues); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ba814fb0a5..91c12ed55e 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; @@ -111,6 +111,14 @@ const validateQuery = (query: any): void => { } } + if (query.$nor) { + if (query.$nor instanceof Array && query.$nor.length > 0) { + query.$nor.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $nor format - use an array of at least 1 value.'); + } + } + Object.keys(query).forEach(key => { if (query && query[key] && query[key].$regex) { if (typeof query[key].$options === 'string') {