diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 23d8ad4a8a..d3881d5e2e 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -754,6 +754,109 @@ describe('Parse.Query testing', () => { }); }); + it('containedBy pointer array', (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: { + objects: { + $containedBy: 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(); + }); + }); + + + 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 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: { + 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" }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 9eeef66b89..f1952ea5da 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}; } @@ -797,6 +800,19 @@ function transformConstraint(constraint, field) { answer[key] = s; break; + 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.$elemMatch = { + $nin: arr.map(transformer) + }; + break; + } case '$options': answer[key] = constraint[key]; break; 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';