diff --git a/lib/include.js b/lib/include.js index a88a2458..f3ce0d9c 100644 --- a/lib/include.js +++ b/lib/include.js @@ -1,6 +1,7 @@ var async = require('async'); var utils = require('./utils'); var List = require('./list'); +var includeUtils = require('./include_utils'); var isPlainObject = utils.isPlainObject; var defineCachedRelations = utils.defineCachedRelations; var uniq = utils.uniq; @@ -267,6 +268,11 @@ Inclusion.include = function (objects, include, options, cb) { if (relation.type === 'referencesMany') { return includeReferencesMany(cb); } + + //This handles exactly hasMany. Fast and straightforward. Without parallel, each and other boilerplate. + if(relation.type === 'hasMany' && relation.multiple && !subInclude){ + return includeHasManySimple(cb); + } //assuming all other relations with multiple=true as hasMany return includeHasMany(cb); } @@ -482,6 +488,35 @@ Inclusion.include = function (objects, include, options, cb) { } } + /** + * Handle inclusion of HasMany relation + * @param callback + */ + function includeHasManySimple(callback) { + //Map for Indexing objects by their id for faster retrieval + var objIdMap2 = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, relation.keyFrom); + + filter.where[relation.keyTo] = { + inq: uniq(objIdMap2.getKeys()) + }; + + relation.applyScope(null, filter); + relation.modelTo.find(filter, options, targetFetchHandler); + + function targetFetchHandler(err, targets) { + if(err) { + return callback(err); + } + var targetsIdMap = includeUtils.buildOneToManyIdentityMapWithOrigKeys(targets, relation.keyTo); + includeUtils.join(objIdMap2, targetsIdMap, function(obj1, valueToMergeIn){ + defineCachedRelations(obj1); + obj1.__cachedRelations[relationName] = valueToMergeIn; + processTargetObj(obj1, function(){}); + }); + callback(err, objs); + } + } + /** * Handle inclusion of HasMany relation * @param callback @@ -808,7 +843,9 @@ Inclusion.include = function (objects, include, options, cb) { * @returns {*} */ function processTargetObj(obj, callback) { - var inst = (obj instanceof self) ? obj : new self(obj); + + var isInst = obj instanceof self; + // Calling the relation method on the instance if (relation.type === 'belongsTo') { // If the belongsTo relation doesn't have an owner @@ -816,7 +853,7 @@ Inclusion.include = function (objects, include, options, cb) { defineCachedRelations(obj); // Set to null if the owner doesn't exist obj.__cachedRelations[relationName] = null; - if (obj === inst) { + if (isInst) { obj.__data[relationName] = null; } else { obj[relationName] = null; @@ -830,7 +867,7 @@ Inclusion.include = function (objects, include, options, cb) { * @param cb */ function setIncludeData(result, cb) { - if (obj === inst) { + if (isInst) { if (Array.isArray(result) && !(result instanceof List)) { result = new List(result, relation.modelTo); } @@ -848,6 +885,9 @@ Inclusion.include = function (objects, include, options, cb) { return setIncludeData(obj.__cachedRelations[relationName], callback); } + + var inst = (obj instanceof self) ? obj : new self(obj); + //If related objects are not cached by include Handlers, directly call //related accessor function even though it is not very efficient var related; // relation accessor function diff --git a/lib/include_utils.js b/lib/include_utils.js new file mode 100644 index 00000000..528d855f --- /dev/null +++ b/lib/include_utils.js @@ -0,0 +1,99 @@ +module.exports.buildOneToOneIdentityMapWithOrigKeys = buildOneToOneIdentityMapWithOrigKeys; +module.exports.buildOneToManyIdentityMapWithOrigKeys = buildOneToManyIdentityMapWithOrigKeys; +module.exports.join = join; +module.exports.KVMap = KVMap; + +/** + * Effectively builds associative map on id -> object relation and stores original keys. + * Map returned in form of object with ids in keys and object as values. + * @param objs array of objects to build from + * @param idName name of property to be used as id. Such property considered to be unique across array. + * In case of collisions last wins. For non-unique ids use buildOneToManyIdentityMap() + * @returns {} object where keys are ids and values are objects itself + */ +function buildOneToOneIdentityMapWithOrigKeys(objs, idName) { + var kvMap = new KVMap(); + for(var i = 0; i < objs.length; i++) { + var obj = objs[i]; + var id = obj[idName]; + kvMap.set(id, obj); + } + return kvMap; +} + +function buildOneToManyIdentityMapWithOrigKeys(objs, idName) { + var kvMap = new KVMap(); + for(var i = 0; i < objs.length; i++) { + var obj = objs[i]; + var id = obj[idName]; + var value = kvMap.get(id) || []; + value.push(obj); + kvMap.set(id, value); + } + return kvMap; +} + + +/** + * Yeah, it joins. You need three things id -> obj1 map, id -> [obj2] map and merge function. + * This functions will take each obj1, locate all data to join in map2 and call merge function. + * @param oneToOneIdMap + * @param oneToManyIdMap + * @param mergeF function(obj, objectsToMergeIn) + */ +function join(oneToOneIdMap, oneToManyIdMap, mergeF) { + var ids = oneToOneIdMap.getKeys(); + for(var i = 0; i < ids.length; i++) { + var id = ids[i]; + var obj = oneToOneIdMap.get(id); + var objectsToMergeIn = oneToManyIdMap.get(id) || []; + mergeF(obj, objectsToMergeIn); + } +} + + +/** + * Map with arbitrary keys and values. User .set() and .get() to work with values instead of [] + * @returns {{set: Function, get: Function, remove: Function, exist: Function, getKeys: Function}} + * @constructor + */ +function KVMap(){ + var _originalKeyFieldName = 'originalKey'; + var _valueKeyFieldName = 'value'; + var _dict = {}; + var keyToString = function(key){ return key.toString() }; + var mapImpl = { + set: function(key, value){ + var recordObj = {}; + recordObj[_originalKeyFieldName] = key; + recordObj[_valueKeyFieldName] = value; + _dict[keyToString(key)] = recordObj; + return true; + }, + get: function(key){ + var storeObj = _dict[keyToString(key)]; + if(storeObj) { + return storeObj[_valueKeyFieldName]; + } else { + return undefined; + } + }, + remove: function(key){ + delete _dict[keyToString(key)]; + return true; + }, + exist: function(key) { + var result = _dict.hasOwnProperty(keyToString(key)); + return result; + }, + getKeys: function(){ + var result = []; + for(var key in _dict) { + result.push(_dict[key][_originalKeyFieldName]); + } + return result; + } + + }; + return mapImpl; +} diff --git a/test/include.test.js b/test/include.test.js index c9b82bee..3f954a70 100644 --- a/test/include.test.js +++ b/test/include.test.js @@ -56,6 +56,7 @@ describe('include', function () { it('should fetch Passport - Owner - Posts', function (done) { Passport.find({include: {owner: 'posts'}}, function (err, passports) { + should.not.exist(err); should.exist(passports); passports.length.should.be.ok; diff --git a/test/include_util.test.js b/test/include_util.test.js new file mode 100644 index 00000000..280bd5eb --- /dev/null +++ b/test/include_util.test.js @@ -0,0 +1,131 @@ +var assert = require("assert"); +var should = require("should"); + +var includeUtils = require("../lib/include_utils"); + +describe('include_util', function(){ + describe('#buildOneToOneIdentityMapWithOrigKeys', function(){ + it('should return an object with keys', function(){ + var objs = [ + {id: 11, letter: "A"}, + {id: 22, letter: "B"} + ]; + var result = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, "id"); + result.get(11).should.be.ok; + result.get(22).should.be.ok; + }); + + it('should overwrite keys in case of collision', function(){ + var objs = [ + {id: 11, letter: "A"}, + {id: 22, letter: "B"}, + {id: 33, letter: "C"}, + {id: 11, letter: "HA!"} + ]; + + var result = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, "id"); + result.getKeys().should.containEql(11); + result.getKeys().should.containEql(22); + result.getKeys().should.containEql(33); + result.get(11)["letter"].should.equal("HA!"); + result.get(33)["letter"].should.equal("C"); + }); + }); + describe('#buildOneToOneIdentityMapWithOrigKeys', function(){ + it('should return an object with keys', function(){ + var objs = [ + {id: 11, letter: "A"}, + {id: 22, letter: "B"} + ]; + var result = includeUtils.buildOneToOneIdentityMapWithOrigKeys(objs, 'id'); + result.get(11).should.be.ok; + result.get(22).should.be.ok; + result.getKeys().should.have.lengthOf(2); // no additional properties + }); + }); + describe('#buildOneToManyIdentityMap', function(){ + it('should return an object with keys', function(){ + var objs = [ + {id: 11, letter: "A"}, + {id: 22, letter: "B"} + ]; + var result = includeUtils.buildOneToManyIdentityMapWithOrigKeys(objs, "id"); + result.exist(11).should.be.true; + result.exist(22).should.be.true; + }); + + it('should collect keys in case of collision', function(){ + var objs = [ + {fk_id: 11, letter: "A"}, + {fk_id: 22, letter: "B"}, + {fk_id: 33, letter: "C"}, + {fk_id: 11, letter: "HA!"} + ]; + + var result = includeUtils.buildOneToManyIdentityMapWithOrigKeys(objs, "fk_id"); + result.get(11)[0]["letter"].should.equal("A"); + result.get(11)[1]["letter"].should.equal("HA!"); + result.get(33)[0]["letter"].should.equal("C"); + }); + }); +}); + + +describe('KVMap', function(){ + it('should allow to set and get value with key string', function(){ + var map = new includeUtils.KVMap(); + map.set('name', 'Alex'); + map.set('gender', true); + map.set('age', 25); + map.get('name').should.be.equal('Alex'); + map.get('gender').should.be.equal(true); + map.get('age').should.be.equal(25); + }); + it('should allow to set and get value with arbitrary key type', function(){ + var map = new includeUtils.KVMap(); + map.set('name', 'Alex'); + map.set(true, 'male'); + map.set(false, false); + map.set({isTrue: 'yes'}, 25); + map.get('name').should.be.equal('Alex'); + map.get(true).should.be.equal('male'); + map.get(false).should.be.equal(false); + map.get({isTrue: 'yes'}).should.be.equal(25); + }); + it('should not allow to get values with [] operator', function(){ + var map = new includeUtils.KVMap(); + map.set('name', 'Alex'); + (map['name'] === undefined).should.be.equal(true); + }); + it('should provide .exist() method for checking if key presented', function(){ + var map = new includeUtils.KVMap(); + map.set('one', 1); + map.set(2, 'two'); + map.set(true, 'true'); + map.exist('one').should.be.true; + map.exist(2).should.be.true; + map.exist(true).should.be.true; + map.exist('two').should.be.false; + }); + it('should return array of original keys with .getKeys()', function(){ + var map = new includeUtils.KVMap(); + map.set('one', 1); + map.set(2, 'two'); + map.set(true, 'true'); + var keys = map.getKeys(); + keys.should.containEql('one'); + keys.should.containEql(2); + keys.should.containEql(true); + }); + it('should allow to store and fetch arrays', function(){ + var map = new includeUtils.KVMap(); + map.set(1, [1, 2, 3]); + map.set(2, [2, 3, 4]); + var valueOne = map.get(1); + valueOne.should.be.eql([1, 2, 3]); + valueOne.push(99); + map.set(1, valueOne); + var valueOneUpdated = map.get(1); + valueOneUpdated.should.be.eql([1, 2, 3, 99]); + }); +});