loopback-datasource-juggler/test/include.test.js

1771 lines
59 KiB
JavaScript
Raw Normal View History

2019-05-08 15:45:37 +00:00
// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2016-04-01 22:25:16 +00:00
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2016-08-22 19:55:22 +00:00
'use strict';
/* global getSchema:false, connectorCapabilities:false */
2018-12-07 14:54:29 +00:00
const assert = require('assert');
const async = require('async');
const bdd = require('./helpers/bdd-if');
const should = require('./init.js');
2018-12-07 14:54:29 +00:00
const DataSource = require('../').DataSource;
2013-04-06 10:57:12 +00:00
2018-12-07 14:54:29 +00:00
let db, User, Profile, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
2013-03-27 00:46:59 +00:00
2018-12-07 14:54:29 +00:00
const knownUsers = ['User A', 'User B', 'User C', 'User D', 'User E'];
const knownPassports = ['1', '2', '3', '4'];
const knownPosts = ['Post A', 'Post B', 'Post C', 'Post D', 'Post E'];
const knownProfiles = ['Profile A', 'Profile B', 'Profile Z'];
2016-04-01 11:48:17 +00:00
describe('include', function() {
2014-01-24 17:09:53 +00:00
before(setup);
2016-04-01 11:48:17 +00:00
it('should fetch belongsTo relation', function(done) {
2016-08-19 17:46:59 +00:00
Passport.find({include: 'owner'}, function(err, passports) {
2014-01-24 17:09:53 +00:00
passports.length.should.be.ok;
2016-04-01 11:48:17 +00:00
passports.forEach(function(p) {
2014-01-24 17:09:53 +00:00
p.__cachedRelations.should.have.property('owner');
2014-01-28 00:04:37 +00:00
// The relation should be promoted as the 'owner' property
p.should.have.property('owner');
// The __cachedRelations should be removed from json output
p.toJSON().should.not.have.property('__cachedRelations');
2018-12-07 14:54:29 +00:00
const owner = p.__cachedRelations.owner;
2014-01-24 17:09:53 +00:00
if (!p.ownerId) {
should.not.exist(owner);
} else {
should.exist(owner);
owner.id.should.eql(p.ownerId);
2014-01-24 17:09:53 +00:00
}
});
done();
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
it('does not return included item if FK is excluded', function(done) {
Passport.find({include: 'owner', fields: 'number'}, function(err, passports) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const owner = passports[0].toJSON().owner;
should.not.exist(owner);
done();
});
});
2016-04-01 11:48:17 +00:00
it('should fetch hasMany relation', function(done) {
2016-08-19 17:46:59 +00:00
User.find({include: 'posts'}, function(err, users) {
2014-01-24 17:09:53 +00:00
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2016-04-01 11:48:17 +00:00
users.forEach(function(u) {
2014-01-28 00:04:37 +00:00
// The relation should be promoted as the 'owner' property
u.should.have.property('posts');
// The __cachedRelations should be removed from json output
u.toJSON().should.not.have.property('__cachedRelations');
2014-01-24 17:09:53 +00:00
u.__cachedRelations.should.have.property('posts');
2016-04-01 11:48:17 +00:00
u.__cachedRelations.posts.forEach(function(p) {
// FIXME There are cases that p.userId is string
p.userId.toString().should.eql(u.id.toString());
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
done();
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
it('should report errors if the PK is excluded', function(done) {
User.find({include: 'posts', fields: 'name'}, function(err) {
should.exist(err);
err.message.should.match(/ID property "id" is missing/);
done();
});
});
it('should not have changed the __strict flag of the model', function(done) {
const originalStrict = User.definition.settings.strict;
User.definition.settings.strict = true; // Change to test regression for issue #1252
const finish = (err) => {
// Restore original user strict property
User.definition.settings.strict = originalStrict;
done(err);
};
User.find({include: 'posts'}, function(err, users) {
if (err) return finish(err);
users.forEach(user => {
user.should.have.property('__strict', true); // we changed it
});
finish();
});
});
2017-07-11 18:56:39 +00:00
bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
2018-06-12 07:13:32 +00:00
'should not save in db included models, in query returned models',
2017-07-11 18:56:39 +00:00
function(done) {
const originalStrict = User.definition.settings.strict;
User.definition.settings.strict = true; // Change to test regression for issue #1252
const finish = (err) => {
// Restore original user strict property
User.definition.settings.strict = originalStrict;
done(err);
};
User.findOne({where: {name: 'User A'}, include: 'posts'}, function(err, user) {
if (err) return finish(err);
2017-07-11 18:56:39 +00:00
if (!user) return finish(new Error('User Not found to check relation not saved'));
user.save(function(err) { // save the returned user
if (err) return finish(err);
// should not store in db the posts
2018-12-07 14:54:29 +00:00
const dsName = User.dataSource.name;
2017-07-11 18:56:39 +00:00
if (dsName === 'memory') {
JSON.parse(User.dataSource.adapter.cache.User[1]).should.not.have.property('posts');
finish();
} else if (dsName === 'mongodb') { // Check native mongodb connector
// get hold of native mongodb collection
2018-12-07 14:54:29 +00:00
const dbCollection = User.dataSource.connector.collection(User.modelName);
2017-07-11 18:56:39 +00:00
dbCollection.findOne({_id: user.id})
.then(function(foundUser) {
if (!foundUser) {
finish(new Error('User not found to check posts not saved'));
}
foundUser.should.not.have.property('posts');
finish();
})
.catch(finish);
} else { // TODO make native checks for other connectors as well
finish();
}
});
});
});
2016-04-01 11:48:17 +00:00
it('should fetch Passport - Owner - Posts', function(done) {
2016-08-19 17:46:59 +00:00
Passport.find({include: {owner: 'posts'}}, function(err, passports) {
2014-01-24 17:09:53 +00:00
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
2016-04-01 11:48:17 +00:00
passports.forEach(function(p) {
2014-01-24 17:09:53 +00:00
p.__cachedRelations.should.have.property('owner');
2014-01-28 00:04:37 +00:00
// The relation should be promoted as the 'owner' property
p.should.have.property('owner');
// The __cachedRelations should be removed from json output
p.toJSON().should.not.have.property('__cachedRelations');
2018-12-07 14:54:29 +00:00
const user = p.__cachedRelations.owner;
2014-01-24 17:09:53 +00:00
if (!p.ownerId) {
should.not.exist(user);
} else {
should.exist(user);
user.id.should.eql(p.ownerId);
2014-01-24 17:09:53 +00:00
user.__cachedRelations.should.have.property('posts');
user.should.have.property('posts');
user.toJSON().should.have.property('posts').and.be.an.Array;
2016-04-01 11:48:17 +00:00
user.__cachedRelations.posts.forEach(function(pp) {
// FIXME There are cases that pp.userId is string
pp.userId.toString().should.eql(user.id.toString());
2014-01-24 17:09:53 +00:00
});
}
});
done();
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
2016-04-01 11:48:17 +00:00
it('should fetch Passport - Owner - empty Posts', function(done) {
2016-08-19 17:46:59 +00:00
Passport.findOne({where: {number: '4'}, include: {owner: 'posts'}}, function(err, passport) {
should.not.exist(err);
should.exist(passport);
passport.__cachedRelations.should.have.property('owner');
// The relation should be promoted as the 'owner' property
passport.should.have.property('owner');
// The __cachedRelations should be removed from json output
passport.toJSON().should.not.have.property('__cachedRelations');
2018-12-07 14:54:29 +00:00
const user = passport.__cachedRelations.owner;
should.exist(user);
user.id.should.eql(passport.ownerId);
user.__cachedRelations.should.have.property('posts');
user.should.have.property('posts');
2015-12-22 23:03:50 +00:00
user.toJSON().should.have.property('posts').and.be.an.Array().with
2018-06-12 07:13:32 +00:00
.length(0);
done();
});
});
2016-04-01 11:48:17 +00:00
it('should fetch Passport - Owner - Posts - alternate syntax', function(done) {
2016-08-19 17:46:59 +00:00
Passport.find({include: {owner: {relation: 'posts'}}}, function(err, passports) {
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
2018-12-07 14:54:29 +00:00
let posts;
if (connectorCapabilities.adhocSort !== false) {
posts = passports[0].owner().posts();
posts.should.have.length(3);
} else {
if (passports[0].owner()) {
posts = passports[0].owner().posts();
posts.length.should.be.belowOrEqual(3);
}
}
done();
});
});
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
it('should fetch Passports - User - Posts - User', function(done) {
2014-03-13 23:43:38 +00:00
Passport.find({
2016-08-19 17:46:59 +00:00
include: {owner: {posts: 'author'}},
2016-04-01 11:48:17 +00:00
}, function(err, passports) {
2014-01-24 17:09:53 +00:00
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
2016-04-01 11:48:17 +00:00
passports.forEach(function(p) {
2014-01-24 17:09:53 +00:00
p.__cachedRelations.should.have.property('owner');
2018-12-07 14:54:29 +00:00
const user = p.__cachedRelations.owner;
2014-01-24 17:09:53 +00:00
if (!p.ownerId) {
should.not.exist(user);
} else {
should.exist(user);
user.id.should.eql(p.ownerId);
2014-01-24 17:09:53 +00:00
user.__cachedRelations.should.have.property('posts');
2016-04-01 11:48:17 +00:00
user.__cachedRelations.posts.forEach(function(pp) {
2014-10-10 10:28:39 +00:00
pp.should.have.property('id');
// FIXME There are cases that pp.userId is string
pp.userId.toString().should.eql(user.id.toString());
pp.should.have.property('author');
2014-01-24 17:09:53 +00:00
pp.__cachedRelations.should.have.property('author');
2018-12-07 14:54:29 +00:00
const author = pp.__cachedRelations.author;
author.id.should.eql(user.id);
2014-01-24 17:09:53 +00:00
});
}
});
done();
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
2016-04-01 11:48:17 +00:00
it('should fetch Passports with include scope on Posts', function(done) {
2014-10-10 10:28:39 +00:00
Passport.find({
2016-08-19 17:46:59 +00:00
include: {owner: {relation: 'posts', scope: {
2014-10-10 10:28:39 +00:00
fields: ['title'], include: ['author'],
2016-04-01 11:48:17 +00:00
order: 'title DESC',
}}},
}, function(err, passports) {
2014-10-10 10:28:39 +00:00
should.not.exist(err);
should.exist(passports);
2018-12-07 14:54:29 +00:00
let passport, owner, posts;
if (connectorCapabilities.adhocSort !== false) {
passports.length.should.equal(4);
passport = passports[0];
passport.number.should.equal('1');
passport.owner().name.should.equal('User A');
owner = passport.owner().toObject();
posts = passport.owner().posts();
posts.should.be.an.array;
posts.should.have.length(3);
posts[0].title.should.equal('Post C');
posts[0].should.have.property('id', undefined); // omitted
posts[0].author().should.be.instanceOf(User);
posts[0].author().name.should.equal('User A');
posts[1].title.should.equal('Post B');
posts[1].author().name.should.equal('User A');
posts[2].title.should.equal('Post A');
posts[2].author().name.should.equal('User A');
} else {
passports.length.should.be.belowOrEqual(4);
passport = passports[0];
passport.number.should.be.oneOf(knownPassports);
if (passport.owner()) {
passport.owner().name.should.be.oneOf(knownUsers);
owner = passport.owner().toObject();
posts = passport.owner().posts();
posts.should.be.an.array;
posts.length.should.be.belowOrEqual(3);
if (posts[0]) {
posts[0].title.should.be.oneOf(knownPosts);
posts[0].author().should.be.instanceOf(User);
posts[0].author().name.should.be.oneOf(knownUsers);
}
}
}
2014-10-10 10:28:39 +00:00
done();
});
});
bdd.itIf(connectorCapabilities.adhocSort !== false,
2018-06-12 07:13:32 +00:00
'should support limit', function(done) {
Passport.find({
include: {
owner: {
relation: 'posts', scope: {
fields: ['title'], include: ['author'],
order: 'title DESC',
limit: 1,
},
2016-04-01 11:48:17 +00:00
},
},
2018-06-12 07:13:32 +00:00
limit: 2,
}, function(err, passports) {
if (err) return done(err);
passports.length.should.equal(2);
2018-12-07 14:54:29 +00:00
const posts1 = passports[0].toJSON().owner.posts;
2018-06-12 07:13:32 +00:00
posts1.length.should.equal(1);
posts1[0].title.should.equal('Post C');
2018-12-07 14:54:29 +00:00
const posts2 = passports[1].toJSON().owner.posts;
2018-06-12 07:13:32 +00:00
posts2.length.should.equal(1);
posts2[0].title.should.equal('Post D');
2018-06-12 07:13:32 +00:00
done();
});
2015-05-29 17:50:37 +00:00
});
2017-07-11 18:56:39 +00:00
bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
2018-06-12 07:13:32 +00:00
'should support limit - no sort', function(done) {
Passport.find({
include: {
owner: {
relation: 'posts', scope: {
fields: ['title'], include: ['author'],
order: 'title DESC',
limit: 1,
},
},
},
2018-06-12 07:13:32 +00:00
limit: 2,
}, function(err, passports) {
if (err) return done(err);
passports.length.should.equal(2);
2018-12-07 14:54:29 +00:00
let owner = passports[0].toJSON().owner;
2018-06-12 07:13:32 +00:00
if (owner) {
2018-12-07 14:54:29 +00:00
const posts1 = owner.posts;
2018-06-12 07:13:32 +00:00
posts1.length.should.belowOrEqual(1);
if (posts1.length === 1) {
posts1[0].title.should.be.oneOf(knownPosts);
}
}
2018-06-12 07:13:32 +00:00
owner = passports[1].toJSON().owner;
if (owner) {
2018-12-07 14:54:29 +00:00
const posts2 = owner.posts;
2018-06-12 07:13:32 +00:00
posts2.length.should.belowOrEqual(1);
if (posts2.length === 1) {
posts2[0].title.should.be.oneOf(knownPosts);
}
}
2018-06-12 07:13:32 +00:00
done();
});
});
bdd.describeIf(connectorCapabilities.adhocSort !== false,
2018-06-12 07:13:32 +00:00
'inq limit', function() {
before(function() {
Passport.dataSource.settings.inqLimit = 2;
});
2018-06-12 07:13:32 +00:00
after(function() {
delete Passport.dataSource.settings.inqLimit;
});
2018-06-12 07:13:32 +00:00
it('should support include by pagination', function(done) {
// `pagination` in this case is inside the implementation and set by
// `inqLimit = 2` in the before block. This will need to be reworked once
// we decouple `findWithForeignKeysByPage`.
//
// --superkhau
2018-06-12 07:13:32 +00:00
Passport.find({
include: {
owner: {
relation: 'posts',
scope: {
fields: ['title'], include: ['author'],
order: 'title ASC',
},
},
},
2018-06-12 07:13:32 +00:00
}, function(err, passports) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
passports.length.should.equal(4);
2018-12-07 14:54:29 +00:00
const posts1 = passports[0].toJSON().owner.posts;
2018-06-12 07:13:32 +00:00
posts1.length.should.equal(3);
posts1[0].title.should.equal('Post A');
2018-12-07 14:54:29 +00:00
const posts2 = passports[1].toJSON().owner.posts;
2018-06-12 07:13:32 +00:00
posts2.length.should.equal(1);
posts2[0].title.should.equal('Post D');
2018-06-12 07:13:32 +00:00
done();
});
});
});
bdd.describeIf(connectorCapabilities.adhocSort !== false,
2018-06-12 07:13:32 +00:00
'findWithForeignKeysByPage', function() {
context('filter', function() {
it('works when using a `where` with a foreign key', function(done) {
User.findOne({
include: {
relation: 'passports',
},
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const passport = user.passports()[0];
2018-06-12 07:13:32 +00:00
// eql instead of equal because mongo uses object id type
passport.id.should.eql(createdPassports[0].id);
passport.ownerId.should.eql(createdPassports[0].ownerId);
passport.number.should.eql(createdPassports[0].number);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using a `where` with `and`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
where: {
and: [
{id: createdPosts[0].id},
// Remove the duplicate userId to avoid Cassandra failure
// {userId: createdPosts[0].userId},
{title: 'Post A'},
],
},
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
user.name.should.equal('User A');
user.age.should.equal(21);
user.id.should.eql(createdUsers[0].id);
2018-12-07 14:54:29 +00:00
const posts = user.posts();
2018-06-12 07:13:32 +00:00
posts.length.should.equal(1);
2018-12-07 14:54:29 +00:00
const post = posts[0];
2018-06-12 07:13:32 +00:00
post.title.should.equal('Post A');
// eql instead of equal because mongo uses object id type
post.userId.should.eql(createdPosts[0].userId);
post.id.should.eql(createdPosts[0].id);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `limit`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
limit: 1,
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
user.posts().length.should.equal(1);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `skip`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
skip: 1,
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const ids = user.posts().map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
ids.should.eql([createdPosts[1].id, createdPosts[2].id]);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `offset`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
offset: 1,
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const ids = user.posts().map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
ids.should.eql([createdPosts[1].id, createdPosts[2].id]);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` without `limit`, `skip` or `offset`',
function(done) {
User.findOne({include: {relation: 'posts'}}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const posts = user.posts();
const ids = posts.map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
ids.should.eql([
createdPosts[0].id,
createdPosts[1].id,
createdPosts[2].id,
]);
2018-06-12 07:13:32 +00:00
done();
});
});
});
2018-06-12 07:13:32 +00:00
context('pagination', function() {
it('works with the default page size (0) and `inqlimit` is exceeded',
function(done) {
// inqLimit modifies page size in the impl (there is no way to modify
// page size directly as it is hardcoded (once we decouple the func,
// we can use ctor injection to pass in whatever page size we want).
//
// --superkhau
Post.dataSource.settings.inqLimit = 2;
2018-06-12 07:13:32 +00:00
User.find({include: {relation: 'posts'}}, function(err, users) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
users.length.should.equal(5);
2018-06-12 07:13:32 +00:00
delete Post.dataSource.settings.inqLimit;
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when page size is set to 0', function(done) {
Post.dataSource.settings.inqLimit = 0;
2018-06-12 07:13:32 +00:00
User.find({include: {relation: 'posts'}}, function(err, users) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
users.length.should.equal(5);
2018-06-12 07:13:32 +00:00
delete Post.dataSource.settings.inqLimit;
done();
});
});
});
2018-06-12 07:13:32 +00:00
context('relations', function() {
// WARNING
// The code paths for in this suite of tests were verified manually due to
// the tight coupling of the `findWithForeignKeys` in `include.js`.
//
// TODO
// Decouple the utility functions into their own modules and export each
// function individually to allow for unit testing via DI.
//
// --superkhau
2018-06-12 07:13:32 +00:00
it('works when hasOne is called', function(done) {
User.findOne({include: {relation: 'profile'}}, function(err, user) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
user.name.should.equal('User A');
user.age.should.equal(21);
// eql instead of equal because mongo uses object id type
user.id.should.eql(createdUsers[0].id);
2018-12-07 14:54:29 +00:00
const profile = user.profile();
2018-06-12 07:13:32 +00:00
profile.profileName.should.equal('Profile A');
// eql instead of equal because mongo uses object id type
profile.userId.should.eql(createdProfiles[0].userId);
profile.id.should.eql(createdProfiles[0].id);
2018-06-12 07:13:32 +00:00
done();
});
});
it('does not return included item if hasOne is missing the id property', function(done) {
User.findOne({include: {relation: 'profile'}, fields: 'name'}, function(err, user) {
if (err) return done(err);
should.exist(user);
// Convert to JSON as the user instance has `profile` as a relational method
should.not.exist(user.toJSON().profile);
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when hasMany is called', function(done) {
User.findOne({include: {relation: 'posts'}}, function(err, user) {
if (err) return done();
2018-06-12 07:13:32 +00:00
user.name.should.equal('User A');
user.age.should.equal(21);
// eql instead of equal because mongo uses object id type
user.id.should.eql(createdUsers[0].id);
user.posts().length.should.equal(3);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when hasManyThrough is called', function(done) {
2018-12-07 14:54:29 +00:00
const Physician = db.define('Physician', {name: String});
const Patient = db.define('Patient', {name: String});
const Appointment = db.define('Appointment', {
2018-06-12 07:13:32 +00:00
date: {
type: Date,
default: function() {
return new Date();
},
},
2018-06-12 07:13:32 +00:00
});
2018-12-07 14:54:29 +00:00
const Address = db.define('Address', {name: String});
2018-06-12 07:13:32 +00:00
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
Patient.belongsTo(Address);
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'],
function() {
Physician.create(function(err, physician) {
physician.patients.create({name: 'a'}, function(err, patient) {
Address.create({name: 'z'}, function(err, address) {
patient.address(address);
patient.save(function() {
physician.patients({include: 'address'},
2016-09-08 20:05:48 +00:00
function(err, patients) {
if (err) return done(err);
patients.should.have.length(1);
2018-12-07 14:54:29 +00:00
const p = patients[0];
2016-09-08 20:05:48 +00:00
p.name.should.equal('a');
p.addressId.should.eql(patient.addressId);
p.address().id.should.eql(address.id);
p.address().name.should.equal('z');
done();
});
2018-06-12 07:13:32 +00:00
});
});
});
});
});
2018-06-12 07:13:32 +00:00
});
2018-06-12 07:13:32 +00:00
it('works when belongsTo is called', function(done) {
Profile.findOne({include: 'user'}, function(err, profile) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
profile.profileName.should.equal('Profile A');
profile.userId.should.eql(createdProfiles[0].userId);
profile.id.should.eql(createdProfiles[0].id);
2018-12-07 14:54:29 +00:00
const user = profile.user();
2018-06-12 07:13:32 +00:00
user.name.should.equal('User A');
user.age.should.equal(21);
user.id.should.eql(createdUsers[0].id);
2018-06-12 07:13:32 +00:00
done();
});
});
});
2016-04-01 13:23:42 +00:00
});
bdd.describeIf(connectorCapabilities.adhocSort === false,
2018-06-12 07:13:32 +00:00
'findWithForeignKeysByPage', function() {
// eslint-disable-next-line mocha/no-identical-title
context('filter', function() {
it('works when using a `where` with a foreign key', function(done) {
User.findOne({
include: {
relation: 'passports',
},
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const passport = user.passports()[0];
2018-06-12 07:13:32 +00:00
if (passport) {
2018-12-07 14:54:29 +00:00
const knownPassportIds = [];
const knownOwnerIds = [];
2018-06-12 07:13:32 +00:00
createdPassports.forEach(function(p) {
if (p.id) knownPassportIds.push(p.id);
if (p.ownerId) knownOwnerIds.push(p.ownerId.toString());
});
passport.id.should.be.oneOf(knownPassportIds);
// FIXME passport.ownerId may be string
passport.ownerId.toString().should.be.oneOf(knownOwnerIds);
passport.number.should.be.oneOf(knownPassports);
}
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using a `where` with `and`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
where: {
and: [
{id: createdPosts[0].id},
// Remove the duplicate userId to avoid Cassandra failure
// {userId: createdPosts[0].userId},
{title: createdPosts[0].title},
],
},
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
let posts, post;
2018-06-12 07:13:32 +00:00
if (connectorCapabilities.adhocSort !== false) {
user.name.should.equal('User A');
user.age.should.equal(21);
user.id.should.eql(createdUsers[0].id);
posts = user.posts();
posts.length.should.equal(1);
post = posts[0];
2018-06-12 07:13:32 +00:00
post.title.should.equal('Post A');
// eql instead of equal because mongo uses object id type
post.userId.should.eql(createdPosts[0].userId);
post.id.should.eql(createdPosts[0].id);
} else {
user.name.should.be.oneOf(knownUsers);
2018-12-07 14:54:29 +00:00
const knownUserIds = [];
2018-06-12 07:13:32 +00:00
createdUsers.forEach(function(u) {
knownUserIds.push(u.id.toString());
});
2018-06-12 07:13:32 +00:00
user.id.toString().should.be.oneOf(knownUserIds);
posts = user.posts();
if (posts && posts.length > 0) {
post = posts[0];
post.title.should.be.oneOf(knownPosts);
post.userId.toString().should.be.oneOf(knownUserIds);
2018-12-07 14:54:29 +00:00
const knownPostIds = [];
2018-06-12 07:13:32 +00:00
createdPosts.forEach(function(p) {
knownPostIds.push(p.id);
});
post.id.should.be.oneOf(knownPostIds);
}
}
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `limit`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
limit: 1,
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
user.posts().length.should.belowOrEqual(1);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `skip`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
skip: 1, // will be ignored
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const ids = user.posts().map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
if (ids.length > 0) {
2018-12-07 14:54:29 +00:00
const knownPosts = [];
2018-06-12 07:13:32 +00:00
createdPosts.forEach(function(p) {
if (p.id) knownPosts.push(p.id);
});
ids.forEach(function(id) {
if (id) id.should.be.oneOf(knownPosts);
});
}
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` with `offset`', function(done) {
User.findOne({
include: {
relation: 'posts',
scope: {
offset: 1, // will be ignored
},
},
2018-06-12 07:13:32 +00:00
}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const ids = user.posts().map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
if (ids.length > 0) {
2018-12-07 14:54:29 +00:00
const knownPosts = [];
2018-06-12 07:13:32 +00:00
createdPosts.forEach(function(p) {
if (p.id) knownPosts.push(p.id);
});
ids.forEach(function(id) {
if (id) id.should.be.oneOf(knownPosts);
});
}
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when using `where` without `limit`, `skip` or `offset`',
function(done) {
User.findOne({include: {relation: 'posts'}}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const posts = user.posts();
const ids = posts.map(function(p) { return p.id; });
2018-06-12 07:13:32 +00:00
if (ids.length > 0) {
2018-12-07 14:54:29 +00:00
const knownPosts = [];
2018-06-12 07:13:32 +00:00
createdPosts.forEach(function(p) {
if (p.id) knownPosts.push(p.id);
});
ids.forEach(function(id) {
if (id) id.should.be.oneOf(knownPosts);
});
}
2018-06-12 07:13:32 +00:00
done();
});
2018-06-12 07:13:32 +00:00
});
});
2018-06-12 07:13:32 +00:00
// eslint-disable-next-line mocha/no-identical-title
context('pagination', function() {
it('works with the default page size (0) and `inqlimit` is exceeded',
function(done) {
// inqLimit modifies page size in the impl (there is no way to modify
// page size directly as it is hardcoded (once we decouple the func,
// we can use ctor injection to pass in whatever page size we want).
//
// --superkhau
Post.dataSource.settings.inqLimit = 2;
2018-06-12 07:13:32 +00:00
User.find({include: {relation: 'posts'}}, function(err, users) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
users.length.should.equal(5);
2018-06-12 07:13:32 +00:00
delete Post.dataSource.settings.inqLimit;
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when page size is set to 0', function(done) {
Post.dataSource.settings.inqLimit = 0;
2018-06-12 07:13:32 +00:00
User.find({include: {relation: 'posts'}}, function(err, users) {
if (err) return done(err);
2018-06-12 07:13:32 +00:00
users.length.should.equal(5);
2018-06-12 07:13:32 +00:00
delete Post.dataSource.settings.inqLimit;
done();
});
});
});
2018-06-12 07:13:32 +00:00
// eslint-disable-next-line mocha/no-identical-title
context('relations', function() {
// WARNING
// The code paths for in this suite of tests were verified manually due to
// the tight coupling of the `findWithForeignKeys` in `include.js`.
//
// TODO
// Decouple the utility functions into their own modules and export each
// function individually to allow for unit testing via DI.
//
// --superkhau
2018-06-12 07:13:32 +00:00
it('works when hasOne is called', function(done) {
User.findOne({include: {relation: 'profile'}}, function(err, user) {
if (err) return done(err);
2018-12-07 14:54:29 +00:00
const knownUserIds = [];
const knownProfileIds = [];
2018-06-12 07:13:32 +00:00
createdUsers.forEach(function(u) {
// FIXME user.id below might be string, so knownUserIds should match
2018-06-12 07:13:32 +00:00
knownUserIds.push(u.id.toString());
});
createdProfiles.forEach(function(p) {
// knownProfileIds.push(p.id ? p.id.toString() : '');
2018-06-12 07:13:32 +00:00
knownProfileIds.push(p.id);
});
if (user) {
user.name.should.be.oneOf(knownUsers);
// eql instead of equal because mongo uses object id type
2018-06-12 07:13:32 +00:00
user.id.toString().should.be.oneOf(knownUserIds);
2018-12-07 14:54:29 +00:00
const profile = user.profile();
2018-06-12 07:13:32 +00:00
if (profile) {
profile.profileName.should.be.oneOf(knownProfiles);
// eql instead of equal because mongo uses object id type
if (profile.userId) profile.userId.toString().should.be.oneOf(knownUserIds);
profile.id.should.be.oneOf(knownProfileIds);
}
}
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when hasMany is called', function(done) {
User.findOne({include: {relation: 'posts'}}, function(err, user) {
if (err) return done();
2018-12-07 14:54:29 +00:00
const knownUserIds = [];
2018-06-12 07:13:32 +00:00
createdUsers.forEach(function(u) {
knownUserIds.push(u.id);
});
user.name.should.be.oneOf(knownUsers);
// eql instead of equal because mongo uses object id type
user.id.should.be.oneOf(knownUserIds);
user.posts().length.should.be.belowOrEqual(3);
2018-06-12 07:13:32 +00:00
done();
});
});
2018-06-12 07:13:32 +00:00
it('works when hasManyThrough is called', function(done) {
2018-12-07 14:54:29 +00:00
const Physician = db.define('Physician', {name: String});
const Patient = db.define('Patient', {name: String});
const Appointment = db.define('Appointment', {
2018-06-12 07:13:32 +00:00
date: {
type: Date,
default: function() {
return new Date();
},
},
2018-06-12 07:13:32 +00:00
});
2018-12-07 14:54:29 +00:00
const Address = db.define('Address', {name: String});
2018-06-12 07:13:32 +00:00
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment});
Patient.belongsTo(Address);
Appointment.belongsTo(Patient);
Appointment.belongsTo(Physician);
db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'],
function() {
Physician.create(function(err, physician) {
physician.patients.create({name: 'a'}, function(err, patient) {
Address.create({name: 'z'}, function(err, address) {
patient.address(address);
patient.save(function() {
physician.patients({include: 'address'},
function(err, patients) {
if (err) return done(err);
patients.should.have.length(1);
2018-12-07 14:54:29 +00:00
const p = patients[0];
p.name.should.equal('a');
p.addressId.should.eql(patient.addressId);
p.address().id.should.eql(address.id);
p.address().name.should.equal('z');
done();
});
2018-06-12 07:13:32 +00:00
});
});
});
});
});
2018-06-12 07:13:32 +00:00
});
2018-06-12 07:13:32 +00:00
it('works when belongsTo is called', function(done) {
Profile.findOne({include: 'user'}, function(err, profile) {
if (err) return done(err);
if (!profile) return done(); // not every user has progile
2018-12-07 14:54:29 +00:00
const knownUserIds = [];
const knownProfileIds = [];
2018-06-12 07:13:32 +00:00
createdUsers.forEach(function(u) {
knownUserIds.push(u.id.toString());
});
createdProfiles.forEach(function(p) {
if (p.id) knownProfileIds.push(p.id.toString());
});
if (profile) {
profile.profileName.should.be.oneOf(knownProfiles);
if (profile.userId) profile.userId.toString().should.be.oneOf(knownUserIds);
if (profile.id) profile.id.toString().should.be.oneOf(knownProfileIds);
2018-12-07 14:54:29 +00:00
const user = profile.user();
2018-06-12 07:13:32 +00:00
if (user) {
user.name.should.be.oneOf(knownUsers);
user.id.toString().should.be.oneOf(knownUserIds);
}
}
2018-06-12 07:13:32 +00:00
done();
});
});
});
});
bdd.itIf(connectorCapabilities.adhocSort !== false,
2018-06-12 07:13:32 +00:00
'should fetch Users with include scope on Posts - belongsTo',
function(done) {
2016-08-19 17:46:59 +00:00
Post.find({include: {relation: 'author', scope: {fields: ['name']}}},
function(err, posts) {
should.not.exist(err);
should.exist(posts);
posts.length.should.equal(5);
2018-12-07 14:54:29 +00:00
const author = posts[0].author();
author.name.should.equal('User A');
author.should.have.property('id');
author.should.have.property('age', undefined);
done();
});
});
bdd.itIf(connectorCapabilities.adhocSort === false,
2018-06-12 07:13:32 +00:00
'should fetch Users with include scope on Posts - belongsTo - no sort',
function(done) {
Post.find({include: {relation: 'author', scope: {fields: ['name']}}},
function(err, posts) {
should.not.exist(err);
should.exist(posts);
posts.length.should.be.belowOrEqual(5);
2018-12-07 14:54:29 +00:00
const author = posts[0].author();
if (author) {
author.name.should.be.oneOf('User A', 'User B', 'User C', 'User D', 'User E');
author.should.have.property('id');
author.should.have.property('age', undefined);
}
done();
});
});
it('should fetch Users with include scope on Posts - hasMany', function(done) {
User.find({
include: {relation: 'posts', scope: {
order: 'title DESC',
2016-04-01 11:48:17 +00:00
}},
}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.equal(5);
if (connectorCapabilities.adhocSort !== false) {
users[0].name.should.equal('User A');
users[1].name.should.equal('User B');
2018-12-07 14:54:29 +00:00
let posts = users[0].posts();
posts.should.be.an.array;
posts.should.have.length(3);
posts[0].title.should.equal('Post C');
posts[1].title.should.equal('Post B');
posts[2].title.should.equal('Post A');
posts = users[1].posts();
posts.should.be.an.array;
posts.should.have.length(1);
posts[0].title.should.equal('Post D');
} else {
users.forEach(function(u) {
u.name.should.be.oneOf(knownUsers);
2018-12-07 14:54:29 +00:00
const posts = u.posts();
if (posts) {
posts.should.be.an.array;
posts.length.should.be.belowOrEqual(3);
posts.forEach(function(p) {
p.title.should.be.oneOf(knownPosts);
});
}
});
}
done();
});
});
2014-01-24 17:09:53 +00:00
2016-04-01 11:48:17 +00:00
it('should fetch User - Posts AND Passports', function(done) {
2016-08-19 17:46:59 +00:00
User.find({include: ['posts', 'passports']}, function(err, users) {
2014-01-24 17:09:53 +00:00
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2016-04-01 11:48:17 +00:00
users.forEach(function(user) {
2014-01-28 00:04:37 +00:00
// The relation should be promoted as the 'owner' property
user.should.have.property('posts');
user.should.have.property('passports');
2014-02-14 01:43:28 +00:00
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
2014-02-14 01:43:28 +00:00
userObj.should.have.property('posts');
userObj.should.have.property('passports');
userObj.posts.should.be.an.instanceOf(Array);
userObj.passports.should.be.an.instanceOf(Array);
2014-01-28 00:04:37 +00:00
// The __cachedRelations should be removed from json output
2014-02-14 01:43:28 +00:00
userObj.should.not.have.property('__cachedRelations');
2014-01-28 00:04:37 +00:00
2014-01-24 17:09:53 +00:00
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
2016-04-01 11:48:17 +00:00
user.__cachedRelations.posts.forEach(function(p) {
// FIXME there are cases that p.userId is string
p.userId.toString().should.eql(user.id.toString());
2014-01-24 17:09:53 +00:00
});
2016-04-01 11:48:17 +00:00
user.__cachedRelations.passports.forEach(function(pp) {
// FIXME there are cases that p.ownerId is string
pp.ownerId.toString().should.eql(user.id.toString());
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
done();
2013-03-27 00:46:59 +00:00
});
2014-01-24 17:09:53 +00:00
});
2013-03-27 00:46:59 +00:00
it('should fetch User - Posts AND Passports in relation syntax',
function(done) {
2016-08-19 17:46:59 +00:00
User.find({include: [
{relation: 'posts', scope: {
where: {title: 'Post A'},
}},
2016-04-01 11:48:17 +00:00
'passports',
2016-08-19 17:46:59 +00:00
]}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
users.forEach(function(user) {
// The relation should be promoted as the 'owner' property
user.should.have.property('posts');
user.should.have.property('passports');
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
userObj.should.have.property('posts');
userObj.should.have.property('passports');
userObj.posts.should.be.an.instanceOf(Array);
userObj.passports.should.be.an.instanceOf(Array);
// The __cachedRelations should be removed from json output
userObj.should.not.have.property('__cachedRelations');
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
user.__cachedRelations.posts.forEach(function(p) {
// FIXME there are cases that p.userId is string
p.userId.toString().should.eql(user.id.toString());
p.title.should.be.equal('Post A');
});
user.__cachedRelations.passports.forEach(function(pp) {
// FIXME there are cases that p.ownerId is string
pp.ownerId.toString().should.eql(user.id.toString());
});
});
done();
});
});
2016-04-01 11:48:17 +00:00
it('should not fetch User - AccessTokens', function(done) {
2016-08-19 17:46:59 +00:00
User.find({include: ['accesstokens']}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2016-04-01 11:48:17 +00:00
users.forEach(function(user) {
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
userObj.should.not.have.property('accesstokens');
});
done();
});
});
2016-04-01 11:48:17 +00:00
it('should support hasAndBelongsToMany', function(done) {
2016-08-19 17:46:59 +00:00
Assembly.create({name: 'car'}, function(err, assembly) {
Part.create({partNumber: 'engine'}, function(err, part) {
2016-04-01 11:48:17 +00:00
assembly.parts.add(part, function(err, data) {
assembly.parts(function(err, parts) {
2014-10-14 20:47:59 +00:00
should.not.exist(err);
should.exists(parts);
parts.length.should.equal(1);
parts[0].partNumber.should.equal('engine');
2014-10-14 20:47:59 +00:00
// Create a part
2016-08-19 17:46:59 +00:00
assembly.parts.create({partNumber: 'door'}, function(err, part4) {
Assembly.find({include: 'parts'}, function(err, assemblies) {
2014-10-14 20:47:59 +00:00
assemblies.length.should.equal(1);
assemblies[0].parts().length.should.equal(2);
done();
2014-03-13 23:43:38 +00:00
});
});
});
});
});
});
});
2016-04-01 11:48:17 +00:00
it('should fetch User - Profile (HasOne)', function(done) {
2016-08-19 17:46:59 +00:00
User.find({include: ['profile']}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2018-12-07 14:54:29 +00:00
let usersWithProfile = 0;
2016-04-01 11:48:17 +00:00
users.forEach(function(user) {
// The relation should be promoted as the 'owner' property
user.should.have.property('profile');
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
const profile = user.profile();
if (profile) {
profile.should.be.an.instanceOf(Profile);
usersWithProfile++;
2016-04-01 13:23:42 +00:00
} else {
(profile === null).should.be.true;
}
// The __cachedRelations should be removed from json output
userObj.should.not.have.property('__cachedRelations');
user.__cachedRelations.should.have.property('profile');
if (user.__cachedRelations.profile) {
// FIXME there are cases that profile.userId is string
user.__cachedRelations.profile.userId.toString().should.eql(user.id.toString());
usersWithProfile++;
}
});
usersWithProfile.should.equal(2 * 2);
done();
});
});
it('should not throw on fetch User if include is boolean equals true', function(done) {
User.find({include: true}, function(err, users) {
if (err) return done(err);
should.exist(users);
users.should.not.be.empty();
done();
});
});
it('should not throw on fetch User if include is number', function(done) {
User.find({include: 1}, function(err, users) {
if (err) return done(err);
should.exist(users);
users.should.not.be.empty();
done();
});
});
it('should not throw on fetch User if include is symbol', function(done) {
User.find({include: Symbol('include')}, function(err, users) {
if (err) return done(err);
should.exist(users);
users.should.not.be.empty();
done();
});
});
it('should not throw on fetch User if include is function', function(done) {
const include = () => {};
User.find({include}, function(err, users) {
if (err) return done(err);
should.exist(users);
users.should.not.be.empty();
done();
});
});
2014-10-14 20:47:59 +00:00
// Not implemented correctly, see: loopback-datasource-juggler/issues/166
// fixed by DB optimization
2016-04-01 11:48:17 +00:00
it('should support include scope on hasAndBelongsToMany', function(done) {
2016-08-19 17:46:59 +00:00
Assembly.find({include: {relation: 'parts', scope: {
where: {partNumber: 'engine'},
2016-04-01 11:48:17 +00:00
}}}, function(err, assemblies) {
assemblies.length.should.equal(1);
2018-12-07 14:54:29 +00:00
const parts = assemblies[0].parts();
parts.should.have.length(1);
parts[0].partNumber.should.equal('engine');
done();
});
});
2016-01-08 10:09:29 +00:00
it('should save related items separately', function(done) {
User.find({
2016-04-01 11:48:17 +00:00
include: 'posts',
2016-01-08 10:09:29 +00:00
})
.then(function(users) {
2018-12-07 14:54:29 +00:00
const posts = users[0].posts();
if (connectorCapabilities.adhocSort !== false) {
posts.should.have.length(3);
} else {
if (posts) posts.length.should.be.belowOrEqual(3);
}
2016-01-08 10:09:29 +00:00
return users[0].save();
})
.then(function(updatedUser) {
return User.findById(updatedUser.id, {
2016-04-01 11:48:17 +00:00
include: 'posts',
2016-01-08 10:09:29 +00:00
});
})
.then(function(user) {
2018-12-07 14:54:29 +00:00
const posts = user.posts();
if (connectorCapabilities.adhocSort !== false) {
posts.should.have.length(3);
} else {
if (posts) posts.length.should.be.belowOrEqual(3);
}
2016-01-08 10:09:29 +00:00
})
.then(done)
.catch(done);
2016-01-08 10:09:29 +00:00
});
2016-04-01 11:48:17 +00:00
describe('performance', function() {
2018-12-07 14:54:29 +00:00
let all;
2015-05-20 16:13:56 +00:00
beforeEach(function() {
this.called = 0;
2018-12-07 14:54:29 +00:00
const self = this;
2015-05-20 16:13:56 +00:00
all = db.connector.all;
db.connector.all = function(model, filter, options, cb) {
self.called++;
return all.apply(db.connector, arguments);
};
});
2015-05-20 16:13:56 +00:00
afterEach(function() {
db.connector.all = all;
});
2018-12-07 14:54:29 +00:00
const nDBCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 2 : 4;
it('including belongsTo should make only ' + nDBCalls + ' db calls', function(done) {
2018-12-07 14:54:29 +00:00
const self = this;
2016-08-19 17:46:59 +00:00
Passport.find({include: 'owner'}, function(err, passports) {
passports.length.should.be.ok;
2016-04-01 11:48:17 +00:00
passports.forEach(function(p) {
p.__cachedRelations.should.have.property('owner');
// The relation should be promoted as the 'owner' property
p.should.have.property('owner');
// The __cachedRelations should be removed from json output
p.toJSON().should.not.have.property('__cachedRelations');
2018-12-07 14:54:29 +00:00
const owner = p.__cachedRelations.owner;
if (!p.ownerId) {
should.not.exist(owner);
} else {
should.exist(owner);
owner.id.should.eql(p.ownerId);
}
});
self.called.should.eql(nDBCalls);
done();
});
});
2016-04-01 11:48:17 +00:00
it('including hasManyThrough should make only 3 db calls', function(done) {
2018-12-07 14:54:29 +00:00
const self = this;
2016-08-19 17:46:59 +00:00
Assembly.create([{name: 'sedan'}, {name: 'hatchback'},
2018-06-12 07:13:32 +00:00
{name: 'SUV'}],
function(err, assemblies) {
Part.create([{partNumber: 'engine'}, {partNumber: 'bootspace'},
{partNumber: 'silencer'}],
function(err, parts) {
async.each(parts, function(part, next) {
async.each(assemblies, function(assembly, next) {
if (assembly.name === 'SUV') {
return next();
}
if (assembly.name === 'hatchback' &&
part.partNumber === 'bootspace') {
2018-06-12 07:13:32 +00:00
return next();
}
assembly.parts.add(part, function(err, data) {
next();
});
2018-06-12 07:13:32 +00:00
}, next);
}, function(err) {
2018-12-07 14:54:29 +00:00
const autos = connectorCapabilities.supportTwoOrMoreInq !== false ?
2018-06-12 07:13:32 +00:00
['sedan', 'hatchback', 'SUV'] : ['sedan'];
2018-12-07 14:54:29 +00:00
const resultLength = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 1;
const dbCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 5;
2018-06-12 07:13:32 +00:00
self.called = 0;
Assembly.find({
where: {
name: {
inq: autos,
},
},
include: 'parts',
}, function(err, result) {
should.not.exist(err);
should.exists(result);
result.length.should.equal(resultLength);
// Please note the order of assemblies is random
2018-12-07 14:54:29 +00:00
const assemblies = {};
2018-06-12 07:13:32 +00:00
result.forEach(function(r) {
assemblies[r.name] = r;
});
if (autos.indexOf('sedan') >= 0) assemblies.sedan.parts().should.have.length(3);
if (autos.indexOf('hatchback') >= 0) assemblies.hatchback.parts().should.have.length(2);
if (autos.indexOf('SUV') >= 0) assemblies.SUV.parts().should.have.length(0);
self.called.should.eql(dbCalls);
done();
});
2018-06-12 07:13:32 +00:00
});
});
2018-06-12 07:13:32 +00:00
});
});
2018-12-07 14:54:29 +00:00
const dbCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 11;
it('including hasMany should make only ' + dbCalls + ' db calls', function(done) {
2018-12-07 14:54:29 +00:00
const self = this;
2016-08-19 17:46:59 +00:00
User.find({include: ['posts', 'passports']}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2016-04-01 11:48:17 +00:00
users.forEach(function(user) {
// The relation should be promoted as the 'owner' property
user.should.have.property('posts');
user.should.have.property('passports');
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
userObj.should.have.property('posts');
userObj.should.have.property('passports');
userObj.posts.should.be.an.instanceOf(Array);
userObj.passports.should.be.an.instanceOf(Array);
// The __cachedRelations should be removed from json output
userObj.should.not.have.property('__cachedRelations');
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
2016-04-01 11:48:17 +00:00
user.__cachedRelations.posts.forEach(function(p) {
// FIXME p.userId is string in some cases.
if (p.userId) p.userId.toString().should.eql(user.id.toString());
});
2016-04-01 11:48:17 +00:00
user.__cachedRelations.passports.forEach(function(pp) {
// FIXME pp.owerId is string in some cases.
if (pp.owerId) pp.ownerId.toString().should.eql(user.id.toString());
});
});
self.called.should.eql(dbCalls);
done();
});
});
it('should not make n+1 db calls in relation syntax',
2016-04-01 11:48:17 +00:00
function(done) {
2018-12-07 14:54:29 +00:00
const self = this;
2016-08-19 17:46:59 +00:00
User.find({include: [{relation: 'posts', scope: {
where: {title: 'Post A'},
}}, 'passports']}, function(err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.be.ok;
2016-04-01 11:48:17 +00:00
users.forEach(function(user) {
// The relation should be promoted as the 'owner' property
user.should.have.property('posts');
user.should.have.property('passports');
2018-12-07 14:54:29 +00:00
const userObj = user.toJSON();
userObj.should.have.property('posts');
userObj.should.have.property('passports');
userObj.posts.should.be.an.instanceOf(Array);
userObj.passports.should.be.an.instanceOf(Array);
// The __cachedRelations should be removed from json output
userObj.should.not.have.property('__cachedRelations');
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
2016-04-01 11:48:17 +00:00
user.__cachedRelations.posts.forEach(function(p) {
// FIXME p.userId is string in some cases.
p.userId.toString().should.eql(user.id.toString());
p.title.should.be.equal('Post A');
});
2016-04-01 11:48:17 +00:00
user.__cachedRelations.passports.forEach(function(pp) {
// FIXME p.userId is string in some cases.
pp.ownerId.toString().should.eql(user.id.toString());
});
});
self.called.should.eql(dbCalls);
done();
});
});
});
2016-06-03 22:00:21 +00:00
it('should support disableInclude for hasAndBelongsToMany', function() {
2018-12-07 14:54:29 +00:00
const Patient = db.define('Patient', {name: String});
const Doctor = db.define('Doctor', {name: String});
const DoctorPatient = db.define('DoctorPatient');
2016-06-03 22:00:21 +00:00
Doctor.hasAndBelongsToMany('patients', {
model: 'Patient',
2016-08-19 17:46:59 +00:00
options: {disableInclude: true},
2016-06-03 22:00:21 +00:00
});
2018-12-07 14:54:29 +00:00
let doctor;
2016-06-03 22:00:21 +00:00
return db.automigrate(['Patient', 'Doctor', 'DoctorPatient']).then(function() {
2016-08-19 17:46:59 +00:00
return Doctor.create({name: 'Who'});
2016-06-03 22:00:21 +00:00
}).then(function(inst) {
doctor = inst;
2016-08-19 17:46:59 +00:00
return doctor.patients.create({name: 'Lazarus'});
2016-06-03 22:00:21 +00:00
}).then(function() {
2016-08-19 17:46:59 +00:00
return Doctor.find({include: ['patients']});
2016-06-03 22:00:21 +00:00
}).then(function(list) {
list.should.have.length(1);
list[0].toJSON().should.not.have.property('patients');
});
});
2013-03-27 00:46:59 +00:00
});
2018-12-07 15:22:36 +00:00
let createdUsers = [];
let createdPassports = [];
let createdProfiles = [];
let createdPosts = [];
2013-03-27 00:46:59 +00:00
function setup(done) {
2014-01-24 17:09:53 +00:00
db = getSchema();
City = db.define('City');
Street = db.define('Street');
Building = db.define('Building');
User = db.define('User', {
name: String,
2016-04-01 11:48:17 +00:00
age: Number,
2014-01-24 17:09:53 +00:00
});
Profile = db.define('Profile', {
2016-04-01 11:48:17 +00:00
profileName: String,
});
AccessToken = db.define('AccessToken', {
2016-04-01 11:48:17 +00:00
token: String,
});
2014-01-24 17:09:53 +00:00
Passport = db.define('Passport', {
2016-04-01 11:48:17 +00:00
number: String,
expirationDate: Date,
2014-01-24 17:09:53 +00:00
});
Post = db.define('Post', {
title: {type: String, index: true},
2014-01-24 17:09:53 +00:00
});
2016-08-19 17:46:59 +00:00
Passport.belongsTo('owner', {model: User});
User.hasMany('passports', {foreignKey: 'ownerId'});
User.hasMany('posts', {foreignKey: 'userId'});
User.hasMany('accesstokens', {
foreignKey: 'userId',
2016-08-19 17:46:59 +00:00
options: {disableInclude: true},
});
2016-08-19 17:46:59 +00:00
Profile.belongsTo('user', {model: User});
User.hasOne('profile', {foreignKey: 'userId'});
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
2014-01-24 17:09:53 +00:00
2014-03-13 23:43:38 +00:00
Assembly = db.define('Assembly', {
2016-04-01 11:48:17 +00:00
name: String,
2014-03-13 23:43:38 +00:00
});
Part = db.define('Part', {
2016-04-01 11:48:17 +00:00
partNumber: String,
2014-03-13 23:43:38 +00:00
});
Assembly.hasAndBelongsToMany(Part);
Part.hasAndBelongsToMany(Assembly);
2016-04-01 11:48:17 +00:00
db.automigrate(function() {
2014-01-24 17:09:53 +00:00
createUsers();
function createUsers() {
clearAndCreate(
User,
[
2016-08-19 17:46:59 +00:00
{name: 'User A', age: 21},
{name: 'User B', age: 22},
{name: 'User C', age: 23},
{name: 'User D', age: 24},
{name: 'User E', age: 25},
2014-01-24 17:09:53 +00:00
],
2016-04-01 11:48:17 +00:00
function(items) {
2014-01-24 17:09:53 +00:00
createdUsers = items;
createPassports();
createAccessTokens();
},
2014-01-24 17:09:53 +00:00
);
}
function createAccessTokens() {
clearAndCreate(
AccessToken,
[
2016-08-19 17:46:59 +00:00
{token: '1', userId: createdUsers[0].id},
{token: '2', userId: createdUsers[1].id},
],
function(items) {},
);
}
2014-01-24 17:09:53 +00:00
function createPassports() {
clearAndCreate(
Passport,
[
2016-08-19 17:46:59 +00:00
{number: '1', ownerId: createdUsers[0].id},
{number: '2', ownerId: createdUsers[1].id},
{number: '3'},
{number: '4', ownerId: createdUsers[2].id},
2014-01-24 17:09:53 +00:00
],
2016-04-01 11:48:17 +00:00
function(items) {
2014-01-24 17:09:53 +00:00
createdPassports = items;
createPosts();
},
2014-01-24 17:09:53 +00:00
);
}
2013-03-27 00:46:59 +00:00
function createProfiles() {
clearAndCreate(
Profile,
[
2016-08-19 17:46:59 +00:00
{profileName: 'Profile A', userId: createdUsers[0].id},
{profileName: 'Profile B', userId: createdUsers[1].id},
{profileName: 'Profile Z'},
],
2016-04-01 11:48:17 +00:00
function(items) {
createdProfiles = items;
done();
},
);
}
2014-01-24 17:09:53 +00:00
function createPosts() {
clearAndCreate(
Post,
[
2016-08-19 17:46:59 +00:00
{title: 'Post A', userId: createdUsers[0].id},
{title: 'Post B', userId: createdUsers[0].id},
{title: 'Post C', userId: createdUsers[0].id},
{title: 'Post D', userId: createdUsers[1].id},
{title: 'Post E'},
2014-01-24 17:09:53 +00:00
],
2016-04-01 11:48:17 +00:00
function(items) {
2014-01-24 17:09:53 +00:00
createdPosts = items;
createProfiles();
},
2014-01-24 17:09:53 +00:00
);
}
});
2013-03-27 00:46:59 +00:00
}
function clearAndCreate(model, data, callback) {
2018-12-07 14:54:29 +00:00
const createdItems = [];
2016-04-01 11:48:17 +00:00
model.destroyAll(function() {
2014-01-24 17:09:53 +00:00
nextItem(null, null);
});
2013-03-27 00:46:59 +00:00
2018-12-07 14:54:29 +00:00
let itemIndex = 0;
2014-01-24 17:09:53 +00:00
function nextItem(err, lastItem) {
if (lastItem !== null) {
createdItems.push(lastItem);
}
if (itemIndex >= data.length) {
callback(createdItems);
return;
2013-03-27 00:46:59 +00:00
}
2014-01-24 17:09:53 +00:00
model.create(data[itemIndex], nextItem);
itemIndex++;
}
2013-03-27 00:46:59 +00:00
}
describe('Model instance with included relation .toJSON()', function() {
2018-12-07 14:54:29 +00:00
let db, ChallengerModel, GameParticipationModel, ResultModel;
before(function(done) {
2016-08-19 17:46:59 +00:00
db = new DataSource({connector: 'memory'});
ChallengerModel = db.createModel('Challenger',
{
2016-04-01 11:48:17 +00:00
name: String,
},
{
relations: {
gameParticipations: {
type: 'hasMany',
model: 'GameParticipation',
2016-04-01 11:48:17 +00:00
foreignKey: '',
},
},
});
GameParticipationModel = db.createModel('GameParticipation',
{
2016-04-01 11:48:17 +00:00
date: Date,
},
{
relations: {
challenger: {
type: 'belongsTo',
model: 'Challenger',
2016-04-01 11:48:17 +00:00
foreignKey: '',
},
results: {
type: 'hasMany',
model: 'Result',
2016-04-01 11:48:17 +00:00
foreignKey: '',
},
},
});
ResultModel = db.createModel('Result', {
points: Number,
}, {
relations: {
gameParticipation: {
type: 'belongsTo',
model: 'GameParticipation',
2016-04-01 11:48:17 +00:00
foreignKey: '',
},
},
});
async.waterfall([
createChallengers,
createGameParticipations,
createResults],
2018-06-12 07:13:32 +00:00
function(err) {
done(err);
});
});
function createChallengers(callback) {
2016-08-19 17:46:59 +00:00
ChallengerModel.create([{name: 'challenger1'}, {name: 'challenger2'}], callback);
}
function createGameParticipations(challengers, callback) {
GameParticipationModel.create([
2016-08-19 17:46:59 +00:00
{challengerId: challengers[0].id, date: Date.now()},
{challengerId: challengers[0].id, date: Date.now()},
], callback);
}
function createResults(gameParticipations, callback) {
ResultModel.create([
2016-08-19 17:46:59 +00:00
{gameParticipationId: gameParticipations[0].id, points: 10},
{gameParticipationId: gameParticipations[0].id, points: 20},
], callback);
}
it('should recursively serialize objects', function(done) {
2018-12-07 14:54:29 +00:00
const filter = {include: {gameParticipations: 'results'}};
ChallengerModel.find(filter, function(err, challengers) {
2018-12-07 14:54:29 +00:00
const levelOneInclusion = challengers[0].toJSON().gameParticipations[0];
assert(levelOneInclusion.__data === undefined, '.__data of a level 1 inclusion is undefined.');
2018-12-07 14:54:29 +00:00
const levelTwoInclusion = challengers[0].toJSON().gameParticipations[0].results[0];
assert(levelTwoInclusion.__data === undefined, '__data of a level 2 inclusion is undefined.');
done();
});
});
});