diff --git a/lib/abstract-class.js b/lib/abstract-class.js index a8cb79a5..5d4a36a4 100644 --- a/lib/abstract-class.js +++ b/lib/abstract-class.js @@ -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) { @@ -384,6 +388,202 @@ AbstractClass.count = function (where, cb) { this.schema.adapter.count(this.modelName, cb, where); }; +/** + * + * + * @param objects + * @param include + */ +AbstractClass.include = function (objects, include, callback, preprocessDataCallback) { + var self = this; + + if ((include.constructor.name == 'Array' && include.length == 0) || (include.constructor.name == 'Object' && Object.keys(include).length == 0)) { + callback(null, objects); + return; + } + + console.log('******************* OLD INCLUDE', include); + include = processIncludeJoin(include); + console.log('******************* NEW INCLUDE', include); + + var keyVals = {}; + var objsByKeys = {}; + + var nbCallbacks = 0; + for (var i = 0; i < include.length; i++) { + var cb = processIncludeItem(objects, include[i], keyVals, objsByKeys); + if (cb !== null) { + nbCallbacks++; + cb(function() { + nbCallbacks--; + if (nbCallbacks == 0) { + callback(null, objects); + } + }); + } + } + + + + /* + async.parallel(callbacks, function(err, results) { + callback(null, objs); + }); + */ + + 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': {}}; + var keysToBeProcessed = {}; + if (!keyVals[relation.keyFrom]) { + objsByKeys[relation.keyFrom] = {}; + for (var j = 0; j < objs.length; j++) { + keysToBeProcessed[objs[j][relation.keyFrom]] = true; + 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 inValues = []; + for (var j = 0; j < keyVals[relation.keyFrom].length; j++) { + 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, dataIncluded) { + var objsIncluded = preprocessDataCallback(relation.modelTo.modelName, dataIncluded); + 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; + } + + + + /* + function processIncludeItem(model, objs, include, keyVals, objsByKeys) { + var relations = model.relations; + + if (include.constructor.name === 'Object') { + var relationName = Object.keys(include)[0]; + var relation = relations[relationName]; + var subInclude = include[relationName]; + } else { + var relationName = include; + var relation = relations[relationName]; + var subInclude = []; + } + + 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]); + } + req['where'][relation.keyTo] = {inq: keyVals[relation.keyFrom]}; + req['include'] = subInclude; + return function(cb) { + relation.model.all(req, function(err, dataIncluded) { + + var objsIncluded = dataIncluded.map(function (obj) { + return self.fromDatabase(relation.model.modelName, obj); + }); + + for (var i = 0; i < objsIncluded.length; i++) { + var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]]; + for (var j = 0; j < objectsFrom.length; j++) { + if (!objectsFrom[j].__cache) { + objectsFrom[j].__cache = {}; + } + if (relation.type == 'hasMany') { + if (!objectsFrom[j].__cache[relationName]) { + objectsFrom[j].__cache[relationName] = []; + } + objectsFrom[j].__cache[relationName].push(objsIncluded[i]); + } else { + objectsFrom[j].__cache[relationName] = objsIncluded[i]; + } + } + } + cb(err, dataIncluded); + }); + }; + } + */ +} + /** * Return string representation of class * @@ -671,6 +871,7 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) { type: 'hasMany', keyFrom: 'id', keyTo: params['foreignKey'], + modelTo: anotherClass, multiple: true }; // each instance of this class should have method named @@ -744,6 +945,7 @@ AbstractClass.belongsTo = function (anotherClass, params) { type: 'belongsTo', keyFrom: params['foreignKey'], keyTo: 'id', + modelTo: anotherClass, multiple: false }; @@ -751,6 +953,10 @@ AbstractClass.belongsTo = function (anotherClass, params) { 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); diff --git a/lib/adapters/mysql.js b/lib/adapters/mysql.js index 5d464029..f57977da 100644 --- a/lib/adapters/mysql.js +++ b/lib/adapters/mysql.js @@ -224,9 +224,20 @@ MySQL.prototype.all = function all(model, filter, callback) { if (err) { return callback(err, []); } - callback(null, data.map(function (obj) { - return self.fromDatabase(model, obj); - })); + var preprocessDataCallback = function(model, data) { + data.map(function (obj) { + return self.fromDatabase(model, obj); + }); + return data; + }; + + var objs = preprocessDataCallback(model, data); + if (filter && filter.include) { + this._models[model].model.include(objs, filter.include, callback, preprocessDataCallback); + } else { + callback(null, objs); + } + }.bind(this)); return sql;