Merge pull request #173 from sdrdis/master
Added support for include key
This commit is contained in:
commit
dc74d64249
|
@ -58,6 +58,10 @@ AbstractClass.prototype._initProperties = function (data, applySetters) {
|
|||
value: {}
|
||||
});
|
||||
|
||||
if (data['__cachedRelations']) {
|
||||
this.__cachedRelations = data['__cachedRelations'];
|
||||
}
|
||||
|
||||
for (var i in data) this.__data[i] = this.__dataWas[i] = data[i];
|
||||
|
||||
if (applySetters && ctor.setter) {
|
||||
|
@ -297,6 +301,7 @@ AbstractClass.find = function find(id, cb) {
|
|||
* @param {Object} params (optional)
|
||||
*
|
||||
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
|
||||
* - include: String, Object or Array. See AbstractClass.include documentation.
|
||||
* - order: String
|
||||
* - limit: Number
|
||||
* - skip: Number
|
||||
|
@ -387,6 +392,150 @@ AbstractClass.count = function (where, cb) {
|
|||
this.schema.adapter.count(this.modelName, cb, where);
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows you to load relations of several objects and optimize numbers of requests.
|
||||
*
|
||||
* @param {Array} objects - array of instances
|
||||
* @param {String}, {Object} or {Array} include - which relations you want to load.
|
||||
* @param {Function} cb - Callback called when relations are loaded
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - User.include(users, 'posts', function() {}); will load all users posts with only one additional request.
|
||||
* - User.include(users, ['posts'], function() {}); // same
|
||||
* - User.include(users, ['posts', 'passports'], function() {}); // will load all users posts and passports with two
|
||||
* additional requests.
|
||||
* - Passport.include(passports, {owner: 'posts'}, function() {}); // will load all passports owner (users), and all
|
||||
* posts of each owner loaded
|
||||
* - Passport.include(passports, {owner: ['posts', 'passports']}); // ...
|
||||
* - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ...
|
||||
*
|
||||
*/
|
||||
AbstractClass.include = function (objects, include, cb) {
|
||||
var self = this;
|
||||
|
||||
if (
|
||||
(include.constructor.name == 'Array' && include.length == 0) ||
|
||||
(include.constructor.name == 'Object' && Object.keys(include).length == 0)
|
||||
) {
|
||||
cb(null, objects);
|
||||
return;
|
||||
}
|
||||
|
||||
include = processIncludeJoin(include);
|
||||
|
||||
var keyVals = {};
|
||||
var objsByKeys = {};
|
||||
|
||||
var nbCallbacks = 0;
|
||||
for (var i = 0; i < include.length; i++) {
|
||||
var callback = processIncludeItem(objects, include[i], keyVals, objsByKeys);
|
||||
if (callback !== null) {
|
||||
nbCallbacks++;
|
||||
callback(function() {
|
||||
nbCallbacks--;
|
||||
if (nbCallbacks == 0) {
|
||||
cb(null, objects);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function processIncludeJoin(ij) {
|
||||
if (typeof ij === 'string') {
|
||||
ij = [ij];
|
||||
}
|
||||
if (ij.constructor.name === 'Object') {
|
||||
var newIj = [];
|
||||
for (var key in ij) {
|
||||
var obj = {};
|
||||
obj[key] = ij[key];
|
||||
newIj.push(obj);
|
||||
}
|
||||
return newIj;
|
||||
}
|
||||
return ij;
|
||||
}
|
||||
|
||||
function processIncludeItem(objs, include, keyVals, objsByKeys) {
|
||||
var relations = self.relations;
|
||||
|
||||
if (include.constructor.name === 'Object') {
|
||||
var relationName = Object.keys(include)[0];
|
||||
var subInclude = include[relationName];
|
||||
} else {
|
||||
var relationName = include;
|
||||
var subInclude = [];
|
||||
}
|
||||
var relation = relations[relationName];
|
||||
|
||||
var req = {'where': {}};
|
||||
|
||||
if (!keyVals[relation.keyFrom]) {
|
||||
objsByKeys[relation.keyFrom] = {};
|
||||
for (var j = 0; j < objs.length; j++) {
|
||||
if (!objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]]) {
|
||||
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]] = [];
|
||||
}
|
||||
objsByKeys[relation.keyFrom][objs[j][relation.keyFrom]].push(objs[j]);
|
||||
}
|
||||
keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]);
|
||||
}
|
||||
|
||||
if (keyVals[relation.keyFrom].length > 0) {
|
||||
// deep clone is necessary since inq seems to change the processed array
|
||||
var keysToBeProcessed = {};
|
||||
var inValues = [];
|
||||
for (var j = 0; j < keyVals[relation.keyFrom].length; j++) {
|
||||
keysToBeProcessed[keyVals[relation.keyFrom][j]] = true;
|
||||
if (keyVals[relation.keyFrom][j] !== 'null') {
|
||||
inValues.push(keyVals[relation.keyFrom][j]);
|
||||
}
|
||||
}
|
||||
|
||||
req['where'][relation.keyTo] = {inq: inValues};
|
||||
req['include'] = subInclude;
|
||||
|
||||
return function(cb) {
|
||||
relation.modelTo.all(req, function(err, objsIncluded) {
|
||||
for (var i = 0; i < objsIncluded.length; i++) {
|
||||
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
|
||||
var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
|
||||
for (var j = 0; j < objectsFrom.length; j++) {
|
||||
if (!objectsFrom[j].__cachedRelations) {
|
||||
objectsFrom[j].__cachedRelations = {};
|
||||
}
|
||||
if (relation.multiple) {
|
||||
if (!objectsFrom[j].__cachedRelations[relationName]) {
|
||||
objectsFrom[j].__cachedRelations[relationName] = [];
|
||||
}
|
||||
objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]);
|
||||
} else {
|
||||
objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No relation have been found for these keys
|
||||
for (var key in keysToBeProcessed) {
|
||||
var objectsFrom = objsByKeys[relation.keyFrom][key];
|
||||
for (var j = 0; j < objectsFrom.length; j++) {
|
||||
if (!objectsFrom[j].__cachedRelations) {
|
||||
objectsFrom[j].__cachedRelations = {};
|
||||
}
|
||||
objectsFrom[j].__cachedRelations[relationName] = relation.multiple ? [] : null;
|
||||
}
|
||||
}
|
||||
cb(err, objsIncluded);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string representation of class
|
||||
*
|
||||
|
@ -669,6 +818,14 @@ AbstractClass.prototype.reset = function () {
|
|||
AbstractClass.hasMany = function hasMany(anotherClass, params) {
|
||||
var methodName = params.as; // or pluralize(anotherClass.modelName)
|
||||
var fk = params.foreignKey;
|
||||
|
||||
this.relations[params['as']] = {
|
||||
type: 'hasMany',
|
||||
keyFrom: 'id',
|
||||
keyTo: params['foreignKey'],
|
||||
modelTo: anotherClass,
|
||||
multiple: true
|
||||
};
|
||||
// each instance of this class should have method named
|
||||
// pluralize(anotherClass.modelName)
|
||||
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
|
||||
|
@ -736,10 +893,22 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
|||
var methodName = params.as;
|
||||
var fk = params.foreignKey;
|
||||
|
||||
this.relations[params['as']] = {
|
||||
type: 'belongsTo',
|
||||
keyFrom: params['foreignKey'],
|
||||
keyTo: 'id',
|
||||
modelTo: anotherClass,
|
||||
multiple: false
|
||||
};
|
||||
|
||||
this.schema.defineForeignKey(this.modelName, fk);
|
||||
this.prototype['__finders__'] = this.prototype['__finders__'] || {};
|
||||
|
||||
this.prototype['__finders__'][methodName] = function (id, cb) {
|
||||
if (id === null) {
|
||||
cb(null, null);
|
||||
return;
|
||||
}
|
||||
anotherClass.find(id, function (err,inst) {
|
||||
if (err) return cb(err);
|
||||
if (!inst) return cb(null, null);
|
||||
|
|
|
@ -162,6 +162,7 @@ Schema.prototype.define = function defineClass(className, properties, settings)
|
|||
hiddenProperty(NewClass, 'modelName', className);
|
||||
hiddenProperty(NewClass, 'cache', {});
|
||||
hiddenProperty(NewClass, 'mru', []);
|
||||
hiddenProperty(NewClass, 'relations', {});
|
||||
|
||||
// inherit AbstractClass methods
|
||||
for (var i in AbstractClass) {
|
||||
|
|
|
@ -33,6 +33,7 @@ function it(name, cases) {
|
|||
batch[schemaName][name] = cases;
|
||||
}
|
||||
|
||||
|
||||
module.exports = function testSchema(exportCasesHere, schema) {
|
||||
|
||||
batch = exportCasesHere;
|
||||
|
@ -77,6 +78,27 @@ Object.defineProperty(module.exports, 'it', {
|
|||
value: it
|
||||
});
|
||||
|
||||
|
||||
function clearAndCreate(model, data, callback) {
|
||||
var createdItems = [];
|
||||
model.destroyAll(function () {
|
||||
nextItem(null, null);
|
||||
});
|
||||
|
||||
var itemIndex = 0;
|
||||
function nextItem(err, lastItem) {
|
||||
if (lastItem !== null) {
|
||||
createdItems.push(lastItem);
|
||||
}
|
||||
if (itemIndex >= data.length) {
|
||||
callback(createdItems);
|
||||
return;
|
||||
}
|
||||
model.create(data[itemIndex], nextItem);
|
||||
itemIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
function testOrm(schema) {
|
||||
var requestsAreCounted = schema.name !== 'mongodb';
|
||||
|
||||
|
@ -141,6 +163,7 @@ function testOrm(schema) {
|
|||
});
|
||||
|
||||
Passport.belongsTo(User, {as: 'owner', foreignKey: 'ownerId'});
|
||||
User.hasMany(Passport, {as: 'passports', foreignKey: 'ownerId'});
|
||||
|
||||
var user = new User;
|
||||
|
||||
|
@ -453,6 +476,30 @@ function testOrm(schema) {
|
|||
});
|
||||
});
|
||||
|
||||
if (
|
||||
!schema.name.match(/redis/) &&
|
||||
schema.name !== 'memory' &&
|
||||
schema.name !== 'neo4j' &&
|
||||
schema.name !== 'cradle'
|
||||
)
|
||||
it('relations key is working', function (test) {
|
||||
test.ok(User.relations, 'Relations key should be defined');
|
||||
test.ok(User.relations.posts, 'posts relation should exist on User');
|
||||
test.equal(User.relations.posts.type, 'hasMany', 'Type of hasMany relation is hasMany');
|
||||
test.equal(User.relations.posts.multiple, true, 'hasMany relations are multiple');
|
||||
test.equal(User.relations.posts.keyFrom, 'id', 'keyFrom is primary key of model table');
|
||||
test.equal(User.relations.posts.keyTo, 'userId', 'keyTo is foreign key of related model table');
|
||||
|
||||
test.ok(Post.relations, 'Relations key should be defined');
|
||||
test.ok(Post.relations.author, 'author relation should exist on Post');
|
||||
test.equal(Post.relations.author.type, 'belongsTo', 'Type of belongsTo relation is belongsTo');
|
||||
test.equal(Post.relations.author.multiple, false, 'belongsTo relations are not multiple');
|
||||
test.equal(Post.relations.author.keyFrom, 'userId', 'keyFrom is foreign key of model table');
|
||||
test.equal(Post.relations.author.keyTo, 'id', 'keyTo is primary key of related model table');
|
||||
test.done();
|
||||
});
|
||||
|
||||
|
||||
it('should handle hasMany relationship', function (test) {
|
||||
User.create(function (err, u) {
|
||||
if (err) return console.log(err);
|
||||
|
@ -470,7 +517,6 @@ function testOrm(schema) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
it('hasMany should support additional conditions', function (test) {
|
||||
|
||||
User.create(function (e, u) {
|
||||
|
@ -589,6 +635,235 @@ function testOrm(schema) {
|
|||
};
|
||||
});
|
||||
|
||||
if (
|
||||
schema.name === 'mysql' ||
|
||||
schema.name === 'sqlite3' ||
|
||||
schema.name === 'postgres'
|
||||
)
|
||||
it('should handle include function', function (test) {
|
||||
var createdUsers = [];
|
||||
var createdPassports = [];
|
||||
var createdPosts = [];
|
||||
var context = null;
|
||||
|
||||
createUsers();
|
||||
function createUsers() {
|
||||
clearAndCreate(
|
||||
User,
|
||||
[
|
||||
{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}
|
||||
],
|
||||
function(items) {
|
||||
createdUsers = items;
|
||||
createPassports();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createPassports() {
|
||||
clearAndCreate(
|
||||
Passport,
|
||||
[
|
||||
{number: '1', ownerId: createdUsers[0].id},
|
||||
{number: '2', ownerId: createdUsers[1].id},
|
||||
{number: '3'}
|
||||
],
|
||||
function(items) {
|
||||
createdPassports = items;
|
||||
createPosts();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createPosts() {
|
||||
clearAndCreate(
|
||||
Post,
|
||||
[
|
||||
{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'}
|
||||
],
|
||||
function(items) {
|
||||
createdPosts = items;
|
||||
makeTests();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function makeTests() {
|
||||
var unitTests = [
|
||||
function() {
|
||||
context = ' (belongsTo simple string from passports to users)';
|
||||
Passport.all({include: 'owner'}, testPassportsUser);
|
||||
},
|
||||
function() {
|
||||
context = ' (belongsTo simple string from posts to users)';
|
||||
Post.all({include: 'author'}, testPostsUser);
|
||||
},
|
||||
function() {
|
||||
context = ' (belongsTo simple array)';
|
||||
Passport.all({include: ['owner']}, testPassportsUser);
|
||||
},
|
||||
function() {
|
||||
context = ' (hasMany simple string from users to posts)';
|
||||
User.all({include: 'posts'}, testUsersPosts);
|
||||
},
|
||||
function() {
|
||||
context = ' (hasMany simple string from users to passports)';
|
||||
User.all({include: 'passports'}, testUsersPassports);
|
||||
},
|
||||
function() {
|
||||
context = ' (hasMany simple array)';
|
||||
User.all({include: ['posts']}, testUsersPosts);
|
||||
},
|
||||
function() {
|
||||
context = ' (Passports - User - Posts in object)';
|
||||
Passport.all({include: {'owner': 'posts'}}, testPassportsUserPosts);
|
||||
},
|
||||
function() {
|
||||
context = ' (Passports - User - Posts in array)';
|
||||
Passport.all({include: [{'owner': 'posts'}]}, testPassportsUserPosts);
|
||||
},
|
||||
function() {
|
||||
context = ' (Passports - User - Posts - User)';
|
||||
Passport.all({include: {'owner': {'posts': 'author'}}}, testPassportsUserPosts);
|
||||
},
|
||||
function() {
|
||||
context = ' (User - Posts AND Passports)';
|
||||
User.all({include: ['posts', 'passports']}, testUsersPostsAndPassports);
|
||||
}
|
||||
];
|
||||
|
||||
function testPassportsUser(err, passports, callback) {
|
||||
testBelongsTo(passports, 'owner', callback);
|
||||
}
|
||||
|
||||
function testPostsUser(err, posts, callback) {
|
||||
testBelongsTo(posts, 'author', callback);
|
||||
}
|
||||
|
||||
function testBelongsTo(items, relationName, callback) {
|
||||
if (typeof callback === 'undefined') {
|
||||
callback = nextUnitTest;
|
||||
}
|
||||
var nbInitialRequests = nbSchemaRequests;
|
||||
var nbItemsRemaining = items.length;
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
testItem(items[i]);
|
||||
}
|
||||
|
||||
function testItem(item) {
|
||||
var relation = item.constructor.relations[relationName];
|
||||
var modelNameFrom = item.constructor.modelName;
|
||||
var modelNameTo = relation.modelTo.modelName;
|
||||
item[relationName](function(err, relatedItem) {
|
||||
if (relatedItem !== null) {
|
||||
test.equal(relatedItem[relation.keyTo], item[relation.keyFrom], modelNameTo + '\'s instance match ' + modelNameFrom + '\'s instance' + context);
|
||||
} else {
|
||||
test.ok(item[relation.keyFrom] == null, 'User match passport even when user is null.' + context);
|
||||
}
|
||||
nbItemsRemaining--;
|
||||
if (nbItemsRemaining == 0) {
|
||||
requestsAreCounted && test.equal(nbSchemaRequests, nbInitialRequests, 'No more request have been executed for loading ' + relationName + ' relation' + context)
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function testUsersPosts(err, users, expectedUserNumber, callback) {
|
||||
if (typeof expectedUserNumber === 'undefined') {
|
||||
expectedUserNumber = 5;
|
||||
}
|
||||
test.equal(users.length, expectedUserNumber, 'Exactly ' + expectedUserNumber + ' users returned by query' + context);
|
||||
testHasMany(users, 'posts', callback);
|
||||
}
|
||||
|
||||
function testUsersPassports(err, users, callback) {
|
||||
testHasMany(users, 'passports', callback);
|
||||
}
|
||||
|
||||
function testHasMany(items, relationName, callback) {
|
||||
if (typeof callback === 'undefined') {
|
||||
callback = nextUnitTest;
|
||||
}
|
||||
var nbInitialRequests = nbSchemaRequests;
|
||||
var nbItemRemaining = items.length;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
testItem(items[i]);
|
||||
}
|
||||
|
||||
function testItem(item) {
|
||||
var relation = item.constructor.relations[relationName];
|
||||
var modelNameFrom = item.constructor.modelName;
|
||||
var modelNameTo = relation.modelTo.modelName;
|
||||
item[relationName](function(err, relatedItems) {
|
||||
for (var j = 0; j < relatedItems.length; j++) {
|
||||
test.equal(relatedItems[j][relation.keyTo], item[relation.keyFrom], modelNameTo + '\'s instances match ' + modelNameFrom + '\'s instance' + context);
|
||||
}
|
||||
nbItemRemaining--;
|
||||
if (nbItemRemaining == 0) {
|
||||
requestsAreCounted && test.equal(nbSchemaRequests, nbInitialRequests, 'No more request have been executed for loading ' + relationName + ' relation' + context)
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function testPassportsUserPosts(err, passports) {
|
||||
testPassportsUser(err, passports, function() {
|
||||
var nbPassportsRemaining = passports.length;
|
||||
for (var i = 0; i < passports.length; i++) {
|
||||
if (passports[i].ownerId !== null) {
|
||||
passports[i].owner(function(err, user) {
|
||||
testUsersPosts(null, [user], 1, function() {
|
||||
nextPassport();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
nextPassport();
|
||||
}
|
||||
}
|
||||
function nextPassport() {
|
||||
nbPassportsRemaining--
|
||||
if (nbPassportsRemaining == 0) {
|
||||
nextUnitTest();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function testUsersPostsAndPassports(err, users) {
|
||||
testUsersPosts(err, users, 5, function() {
|
||||
testUsersPassports(err, users, function() {
|
||||
nextUnitTest();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var testNum = 0;
|
||||
function nextUnitTest() {
|
||||
if (testNum >= unitTests.length) {
|
||||
test.done();
|
||||
return;
|
||||
}
|
||||
unitTests[testNum]();
|
||||
testNum++;
|
||||
|
||||
}
|
||||
|
||||
nextUnitTest();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('should destroy all records', function (test) {
|
||||
Post.destroyAll(function (err) {
|
||||
if (err) {
|
||||
|
|
Loading…
Reference in New Issue