'use strict'; // This is a port of the test suite: // hungry/js/test/parse_relation_test.js const ChildObject = Parse.Object.extend({ className: 'ChildObject' }); const ParentObject = Parse.Object.extend({ className: 'ParentObject' }); describe('Parse.Relation testing', () => { it('simple add and remove relation', done => { const child = new ChildObject(); child.set('x', 2); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('child'); child .save() .then( () => { relation.add(child); return parent.save(); }, e => { fail(e); } ) .then(() => { return relation.query().find(); }) .then(list => { equal(list.length, 1, 'Should have gotten one element back'); equal(list[0].id, child.id, 'Should have gotten the right value'); ok(!parent.dirty('child'), 'The relation should not be dirty'); relation.remove(child); return parent.save(); }) .then(() => { return relation.query().find(); }) .then(list => { equal(list.length, 0, 'Delete should have worked'); ok(!parent.dirty('child'), 'The relation should not be dirty'); done(); }); }); it('query relation without schema', async () => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); let relation = parent.relation('child'); relation.add(childObjects[0]); await parent.save(); const parentAgain = new ParentObject(); parentAgain.id = parent.id; relation = parentAgain.relation('child'); const list = await relation.query().find(); equal(list.length, 1, 'Should have gotten one element back'); equal(list[0].id, childObjects[0].id, 'Should have gotten the right value'); }); it('relations are constructed right from query', async () => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('child'); relation.add(childObjects[0]); await parent.save(); const query = new Parse.Query(ParentObject); const object = await query.get(parent.id); const relationAgain = object.relation('child'); const list = await relationAgain.query().find(); equal(list.length, 1, 'Should have gotten one element back'); equal(list[0].id, childObjects[0].id, 'Should have gotten the right value'); ok(!parent.dirty('child'), 'The relation should not be dirty'); }); it('compound add and remove relation', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } let parent; let relation; Parse.Object.saveAll(childObjects) .then(function () { const ParentObject = Parse.Object.extend('ParentObject'); parent = new ParentObject(); parent.set('x', 4); relation = parent.relation('child'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.remove(childObjects[0]); relation.add(childObjects[2]); return parent.save(); }) .then(function () { return relation.query().find(); }) .then(function (list) { equal(list.length, 2, 'Should have gotten two elements back'); ok(!parent.dirty('child'), 'The relation should not be dirty'); relation.remove(childObjects[1]); relation.remove(childObjects[2]); relation.add(childObjects[1]); relation.add(childObjects[0]); return parent.save(); }) .then(function () { return relation.query().find(); }) .then( function (list) { equal(list.length, 2, 'Deletes and then adds should have worked'); ok(!parent.dirty('child'), 'The relation should not be dirty'); done(); }, function (err) { ok(false, err.message); done(); } ); }); it('related at ordering optimizations', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } let parent; let relation; Parse.Object.saveAll(childObjects) .then(function () { const ParentObject = Parse.Object.extend('ParentObject'); parent = new ParentObject(); parent.set('x', 4); relation = parent.relation('child'); relation.add(childObjects); return parent.save(); }) .then(function () { const query = relation.query(); query.descending('createdAt'); query.skip(1); query.limit(3); return query.find(); }) .then(function (list) { expect(list.length).toBe(3); }) .then(done, done.fail); }); it('queries with relations', async () => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('child'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); await parent.save(); const query = relation.query(); query.equalTo('x', 2); const list = await query.find(); equal(list.length, 1, 'There should only be one element'); ok(list[0] instanceof ChildObject, 'Should be of type ChildObject'); equal(list[0].id, childObjects[2].id, 'We should have gotten back the right result'); }); it('queries on relation fields', async () => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('child'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); const relation2 = parent2.relation('child'); relation2.add(childObjects[4]); relation2.add(childObjects[5]); relation2.add(childObjects[6]); const parents = []; parents.push(parent); parents.push(parent2); await Parse.Object.saveAll(parents); const query = new Parse.Query(ParentObject); const objects = []; objects.push(childObjects[4]); objects.push(childObjects[9]); const list = await query.containedIn('child', objects).find(); equal(list.length, 1, 'There should be only one result'); equal(list[0].id, parent2.id, 'Should have gotten back the right result'); }); it('queries on relation fields with multiple containedIn (regression test for #1271)', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } Parse.Object.saveAll(childObjects) .then(() => { const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const parent1Children = parent.relation('child'); parent1Children.add(childObjects[0]); parent1Children.add(childObjects[1]); parent1Children.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); const parent2Children = parent2.relation('child'); parent2Children.add(childObjects[4]); parent2Children.add(childObjects[5]); parent2Children.add(childObjects[6]); const parent2OtherChildren = parent2.relation('otherChild'); parent2OtherChildren.add(childObjects[0]); parent2OtherChildren.add(childObjects[1]); parent2OtherChildren.add(childObjects[2]); return Parse.Object.saveAll([parent, parent2]); }) .then(() => { const objectsWithChild0InBothChildren = new Parse.Query(ParentObject); objectsWithChild0InBothChildren.containedIn('child', [childObjects[0]]); objectsWithChild0InBothChildren.containedIn('otherChild', [childObjects[0]]); return objectsWithChild0InBothChildren.find(); }) .then(objectsWithChild0InBothChildren => { //No parent has child 0 in both it's "child" and "otherChild" field; expect(objectsWithChild0InBothChildren.length).toEqual(0); }) .then(() => { const objectsWithChild4andOtherChild1 = new Parse.Query(ParentObject); objectsWithChild4andOtherChild1.containedIn('child', [childObjects[4]]); objectsWithChild4andOtherChild1.containedIn('otherChild', [childObjects[1]]); return objectsWithChild4andOtherChild1.find(); }) .then(objects => { // parent2 has child 4 and otherChild 1 expect(objects.length).toEqual(1); done(); }); }); it('query on pointer and relation fields with equal', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } Parse.Object.saveAll(childObjects) .then(() => { const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('toChilds'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); parent2.set('toChild', childObjects[2]); const parents = []; parents.push(parent); parents.push(parent2); parents.push(new ParentObject()); return Parse.Object.saveAll(parents).then(() => { const query = new Parse.Query(ParentObject); query.equalTo('objectId', parent.id); query.equalTo('toChilds', childObjects[2]); return query.find().then(list => { equal(list.length, 1, 'There should be 1 result'); done(); }); }); }) .catch(err => { jfail(err); done(); }); }); it('query on pointer and relation fields with equal bis', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } Parse.Object.saveAll(childObjects).then(() => { const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('toChilds'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); parent2.relation('toChilds').add(childObjects[2]); const parents = []; parents.push(parent); parents.push(parent2); parents.push(new ParentObject()); return Parse.Object.saveAll(parents).then(() => { const query = new Parse.Query(ParentObject); query.equalTo('objectId', parent2.id); // childObjects[2] is in 2 relations // before the fix, that woul yield 2 results query.equalTo('toChilds', childObjects[2]); return query.find().then(list => { equal(list.length, 1, 'There should be 1 result'); done(); }); }); }); }); it('or queries on pointer and relation fields', done => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } Parse.Object.saveAll(childObjects).then(() => { const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('toChilds'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); parent2.set('toChild', childObjects[2]); const parents = []; parents.push(parent); parents.push(parent2); parents.push(new ParentObject()); return Parse.Object.saveAll(parents).then(() => { const query1 = new Parse.Query(ParentObject); query1.containedIn('toChilds', [childObjects[2]]); const query2 = new Parse.Query(ParentObject); query2.equalTo('toChild', childObjects[2]); const query = Parse.Query.or(query1, query2); return query.find().then(list => { const objectIds = list.map(function (item) { return item.id; }); expect(objectIds.indexOf(parent.id)).not.toBe(-1); expect(objectIds.indexOf(parent2.id)).not.toBe(-1); equal(list.length, 2, 'There should be 2 results'); done(); }); }); }); }); it('or queries with base constraint on relation field', async () => { const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); parent.set('x', 4); const relation = parent.relation('toChilds'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); parent2.set('x', 3); const relation2 = parent2.relation('toChilds'); relation2.add(childObjects[0]); relation2.add(childObjects[1]); relation2.add(childObjects[2]); const parents = []; parents.push(parent); parents.push(parent2); parents.push(new ParentObject()); await Parse.Object.saveAll(parents); const query1 = new Parse.Query(ParentObject); query1.equalTo('x', 4); const query2 = new Parse.Query(ParentObject); query2.equalTo('x', 3); const query = Parse.Query.or(query1, query2); query.equalTo('toChilds', childObjects[2]); const list = await query.find(); const objectIds = list.map(item => item.id); expect(objectIds.indexOf(parent.id)).not.toBe(-1); expect(objectIds.indexOf(parent2.id)).not.toBe(-1); equal(list.length, 2, 'There should be 2 results'); }); it('Get query on relation using un-fetched parent object', done => { // Setup data model const Wheel = Parse.Object.extend('Wheel'); const Car = Parse.Object.extend('Car'); const origWheel = new Wheel(); origWheel .save() .then(function () { const car = new Car(); const relation = car.relation('wheels'); relation.add(origWheel); return car.save(); }) .then(function (car) { // Test starts here. // Create an un-fetched shell car object const unfetchedCar = new Car(); unfetchedCar.id = car.id; const relation = unfetchedCar.relation('wheels'); const query = relation.query(); // Parent object is un-fetched, so this will call /1/classes/Car instead // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. return query.get(origWheel.id); }) .then(function (wheel) { // Make sure this is Wheel and not Car. strictEqual(wheel.className, 'Wheel'); strictEqual(wheel.id, origWheel.id); }) .then( function () { done(); }, function (err) { ok(false, 'unexpected error: ' + JSON.stringify(err)); done(); } ); }); it('Find query on relation using un-fetched parent object', done => { // Setup data model const Wheel = Parse.Object.extend('Wheel'); const Car = Parse.Object.extend('Car'); const origWheel = new Wheel(); origWheel .save() .then(function () { const car = new Car(); const relation = car.relation('wheels'); relation.add(origWheel); return car.save(); }) .then(function (car) { // Test starts here. // Create an un-fetched shell car object const unfetchedCar = new Car(); unfetchedCar.id = car.id; const relation = unfetchedCar.relation('wheels'); const query = relation.query(); // Parent object is un-fetched, so this will call /1/classes/Car instead // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. return query.find(); }) .then(function (results) { // Make sure this is Wheel and not Car. const wheel = results[0]; strictEqual(wheel.className, 'Wheel'); strictEqual(wheel.id, origWheel.id); }) .then( function () { done(); }, function (err) { ok(false, 'unexpected error: ' + JSON.stringify(err)); done(); } ); }); it('Find objects with a related object using equalTo', done => { // Setup the objects const Card = Parse.Object.extend('Card'); const House = Parse.Object.extend('House'); const card = new Card(); card .save() .then(() => { const house = new House(); const relation = house.relation('cards'); relation.add(card); return house.save(); }) .then(() => { const query = new Parse.Query('House'); query.equalTo('cards', card); return query.find(); }) .then(results => { expect(results.length).toEqual(1); done(); }); }); it('should properly get related objects with unfetched queries', done => { const objects = []; const owners = []; const allObjects = []; // Build 10 Objects and 10 owners while (objects.length != 10) { const object = new Parse.Object('AnObject'); object.set({ index: objects.length, even: objects.length % 2 == 0, }); objects.push(object); const owner = new Parse.Object('AnOwner'); owners.push(owner); allObjects.push(object); allObjects.push(owner); } const anotherOwner = new Parse.Object('AnotherOwner'); return Parse.Object.saveAll(allObjects.concat([anotherOwner])) .then(() => { // put all the AnObject into the anotherOwner relationKey anotherOwner.relation('relationKey').add(objects); // Set each object[i] into owner[i]; owners.forEach((owner, i) => { owner.set('key', objects[i]); }); return Parse.Object.saveAll(owners.concat([anotherOwner])); }) .then(() => { // Query on the relation of another owner const object = new Parse.Object('AnotherOwner'); object.id = anotherOwner.id; const relationQuery = object.relation('relationKey').query(); // Just get the even ones relationQuery.equalTo('even', true); // Make the query on anOwner const query = new Parse.Query('AnOwner'); // where key match the relation query. query.matchesQuery('key', relationQuery); query.include('key'); return query.find(); }) .then(results => { expect(results.length).toBe(5); results.forEach(result => { expect(result.get('key').get('even')).toBe(true); }); return Promise.resolve(); }) .then(() => { // Query on the relation of another owner const object = new Parse.Object('AnotherOwner'); object.id = anotherOwner.id; const relationQuery = object.relation('relationKey').query(); // Just get the even ones relationQuery.equalTo('even', true); // Make the query on anOwner const query = new Parse.Query('AnOwner'); // where key match the relation query. query.doesNotMatchQuery('key', relationQuery); query.include('key'); return query.find(); }) .then( results => { expect(results.length).toBe(5); results.forEach(result => { expect(result.get('key').get('even')).toBe(false); }); done(); }, e => { fail(JSON.stringify(e)); done(); } ); }); it('select query', function (done) { const RestaurantObject = Parse.Object.extend('Restaurant'); const PersonObject = Parse.Object.extend('Person'); const OwnerObject = Parse.Object.extend('Owner'); const restaurants = [ new RestaurantObject({ ratings: 5, location: 'Djibouti' }), new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), ]; const persons = [ new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), new PersonObject({ name: 'Billy', hometown: 'Detroit' }), ]; const owner = new OwnerObject({ name: 'Joe' }); const allObjects = [owner].concat(restaurants).concat(persons); expect(allObjects.length).toEqual(6); Parse.Object.saveAll([owner].concat(restaurants).concat(persons)) .then(function () { owner.relation('restaurants').add(restaurants); return owner.save(); }) .then( async () => { const unfetchedOwner = new OwnerObject(); unfetchedOwner.id = owner.id; const query = unfetchedOwner.relation('restaurants').query(); query.greaterThan('ratings', 4); const mainQuery = new Parse.Query(PersonObject); mainQuery.matchesKeyInQuery('hometown', 'location', query); const results = await mainQuery.find(); equal(results.length, 1); if (results.length > 0) { equal(results[0].get('name'), 'Bob'); } done(); }, e => { fail(JSON.stringify(e)); done(); } ); }); it('dontSelect query', function (done) { const RestaurantObject = Parse.Object.extend('Restaurant'); const PersonObject = Parse.Object.extend('Person'); const OwnerObject = Parse.Object.extend('Owner'); const restaurants = [ new RestaurantObject({ ratings: 5, location: 'Djibouti' }), new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), ]; const persons = [ new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), new PersonObject({ name: 'Billy', hometown: 'Detroit' }), ]; const owner = new OwnerObject({ name: 'Joe' }); const allObjects = [owner].concat(restaurants).concat(persons); expect(allObjects.length).toEqual(6); Parse.Object.saveAll([owner].concat(restaurants).concat(persons)) .then(function () { owner.relation('restaurants').add(restaurants); return owner.save(); }) .then( async () => { const unfetchedOwner = new OwnerObject(); unfetchedOwner.id = owner.id; const query = unfetchedOwner.relation('restaurants').query(); query.greaterThan('ratings', 4); const mainQuery = new Parse.Query(PersonObject); mainQuery.doesNotMatchKeyInQuery('hometown', 'location', query); mainQuery.ascending('name'); const results = await mainQuery.find(); equal(results.length, 2); if (results.length > 0) { equal(results[0].get('name'), 'Billy'); equal(results[1].get('name'), 'Tom'); } done(); }, e => { fail(JSON.stringify(e)); done(); } ); }); it('relations are not bidirectional (regression test for #871)', done => { const PersonObject = Parse.Object.extend('Person'); const p1 = new PersonObject(); const p2 = new PersonObject(); Parse.Object.saveAll([p1, p2]).then(results => { const p1 = results[0]; const p2 = results[1]; const relation = p1.relation('relation'); relation.add(p2); p1.save().then(() => { const query = new Parse.Query(PersonObject); query.equalTo('relation', p1); query.find().then(results => { expect(results.length).toEqual(0); const query = new Parse.Query(PersonObject); query.equalTo('relation', p2); query.find().then(results => { expect(results.length).toEqual(1); expect(results[0].objectId).toEqual(p1.objectId); done(); }); }); }); }); }); it('can query roles in Cloud Code (regession test #1489)', done => { Parse.Cloud.define('isAdmin', request => { const query = new Parse.Query(Parse.Role); query.equalTo('name', 'admin'); return query.first({ useMasterKey: true }).then( role => { const relation = new Parse.Relation(role, 'users'); const admins = relation.query(); admins.equalTo('username', request.user.get('username')); admins.first({ useMasterKey: true }).then( user => { if (user) { done(); } else { fail('Should have found admin user, found nothing instead'); done(); } }, () => { fail('User not admin'); done(); } ); }, error => { fail('Should have found admin user, errored instead'); fail(error); done(); } ); }); const adminUser = new Parse.User(); adminUser.set('username', 'name'); adminUser.set('password', 'pass'); adminUser.signUp().then( adminUser => { const adminACL = new Parse.ACL(); adminACL.setPublicReadAccess(true); // Create admin role const adminRole = new Parse.Role('admin', adminACL); adminRole.getUsers().add(adminUser); adminRole.save().then( () => { Parse.Cloud.run('isAdmin'); }, error => { fail('failed to save role'); fail(error); done(); } ); }, error => { fail('failed to sign up'); fail(error); done(); } ); }); it('can be saved without error', done => { const obj1 = new Parse.Object('PPAP'); obj1.save().then( () => { const newRelation = obj1.relation('aRelation'); newRelation.add(obj1); obj1.save().then( () => { const relation = obj1.get('aRelation'); obj1.set('aRelation', relation); obj1.save().then( () => { done(); }, error => { fail('failed to save ParseRelation object'); fail(error); done(); } ); }, error => { fail('failed to create relation field'); fail(error); done(); } ); }, error => { fail('failed to save obj'); fail(error); done(); } ); }); it('ensures beforeFind on relation doesnt side effect', done => { const parent = new Parse.Object('Parent'); const child = new Parse.Object('Child'); child .save() .then(() => { parent.relation('children').add(child); return parent.save(); }) .then(() => { // We need to use a new reference otherwise the JS SDK remembers the className for a relation // After saves or finds const otherParent = new Parse.Object('Parent'); otherParent.id = parent.id; return otherParent.relation('children').query().find(); }) .then(children => { // Without an after find all is good, all results have been redirected with proper className children.forEach(child => expect(child.className).toBe('Child')); // Setup the afterFind Parse.Cloud.afterFind('Child', req => { return Promise.resolve( req.objects.map(child => { child.set('afterFound', true); return child; }) ); }); const otherParent = new Parse.Object('Parent'); otherParent.id = parent.id; return otherParent.relation('children').query().find(); }) .then(children => { children.forEach(child => { expect(child.className).toBe('Child'); expect(child.get('afterFound')).toBe(true); }); }) .then(done) .catch(done.fail); }); });