Skip to content

Commit

Permalink
Added findOrCreate method
Browse files Browse the repository at this point in the history
  • Loading branch information
biggora committed Feb 20, 2013
1 parent de48183 commit 61506ba
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 36 deletions.
224 changes: 215 additions & 9 deletions lib/abstract-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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) {
Expand Down Expand Up @@ -194,12 +198,15 @@ AbstractClass.create = function (data, callback) {
var data = this.toObject(true); // Added this to fix the beforeCreate trigger not fire.
// The fix is per issue #72 and the fix was found by by5739.

this._adapter().create(modelName, this.constructor._forDB(data), function (err, id) {
this._adapter().create(modelName, this.constructor._forDB(data), function (err, id, rev) {
if (id) {
obj.__data.id = id;
obj.__dataWas.id = id;
defineReadonlyProp(obj, 'id', id);
}
if (rev) {
obj._rev = rev
}
done.call(this, function () {
if (callback) {
callback(err, obj);
Expand Down Expand Up @@ -252,6 +259,34 @@ AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, call
}
};

/**
* Find one record, same as `all`, limited by 1 and return object, not collection,
* if not found, create using data provided as second argument
*
* @param {Object} query - search conditions: {where: {test: 'me'}}.
* @param {Object} data - object to create.
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.findOrCreate = function findOrCreate(query, data, callback) {
if (typeof query === 'undefined') {
query = {where: {}};
}
if (typeof data === 'function' || typeof data === 'undefined') {
callback = data;
data = query && query.where;
}
if (typeof callback === 'undefined') {
callback = function () {};
}

var t = this;
this.findOne(query, function (err, record) {
if (err) return callback(err);
if (record) return callback(null, record);
t.create(data, callback);
});
};

/**
* Check whether object exitst in database
*
Expand Down Expand Up @@ -280,7 +315,9 @@ AbstractClass.find = function find(id, cb) {
this.schema.adapter.find(this.modelName, id, function (err, data) {
var obj = null;
if (data) {
data.id = id;
if (!data.id) {
data.id = id;
}
obj = new this();
obj._initProperties(data, false);
}
Expand All @@ -295,6 +332,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
Expand Down Expand Up @@ -331,8 +369,8 @@ AbstractClass.all = function all(params, cb) {

/**
* Find one record, same as `all`, limited by 1 and return object, not collection
*
* @param {Object} params - search conditions
*
* @param {Object} params - search conditions: {where: {test: 'me'}}
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.findOne = function findOne(params, cb) {
Expand All @@ -344,7 +382,7 @@ AbstractClass.findOne = function findOne(params, cb) {
}
params.limit = 1;
this.all(params, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err);
if (err || !collection || !collection.length > 0) return cb(err, null);
cb(err, collection[0]);
});
};
Expand Down Expand Up @@ -385,6 +423,152 @@ 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);
}
});
} else {
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
*
Expand Down Expand Up @@ -483,7 +667,7 @@ AbstractClass.prototype._adapter = function () {
* Convert instance to Object
*
* @param {Boolean} onlySchema - restrict properties to schema only, default false
* when onlySchema == true, only properties defined in schema returned,
* when onlySchema == true, only properties defined in schema returned,
* otherwise all enumerable properties returned
* @returns {Object} - canonical object representation (no getters and setters)
*/
Expand Down Expand Up @@ -585,6 +769,8 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
var inst = this;
var model = this.constructor.modelName;

if(!data) data = {};

// update instance's properties
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
Expand Down Expand Up @@ -699,6 +885,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);
Expand Down Expand Up @@ -766,14 +960,26 @@ 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);
if (inst[fk] === this.id) {
if (inst.id === this[fk]) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
Expand All @@ -798,12 +1004,12 @@ AbstractClass.belongsTo = function (anotherClass, params) {
this.__cachedRelations[methodName] = p;
} else if (typeof p === 'function') { // acts as async getter
if (typeof cachedValue === 'undefined') {
this.__finders__[methodName](this[fk], function(err, inst) {
this.__finders__[methodName].apply(self, [this[fk], function(err, inst) {
if (!err) {
self.__cachedRelations[methodName] = inst;
}
p(err, inst);
});
}]);
return this[fk];
} else {
p(null, cachedValue);
Expand Down
24 changes: 17 additions & 7 deletions lib/adapters/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Memory.prototype.create = function create(model, data, callback) {
var id = data.id || this.ids[model]++;
data.id = id;
this.cache[model][id] = data;
callback(null, id);
process.nextTick(function () {
callback(null, id);
});
};

Memory.prototype.updateOrCreate = function (model, data, callback) {
Expand All @@ -39,20 +41,26 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {

Memory.prototype.save = function save(model, data, callback) {
this.cache[model][data.id] = data;
callback(null, data);
process.nextTick(function () {
callback(null, data);
});
};

Memory.prototype.exists = function exists(model, id, callback) {
callback(null, this.cache[model].hasOwnProperty(id));
process.nextTick(function () {
callback(null, this.cache[model].hasOwnProperty(id));
}.bind(this));
};

Memory.prototype.find = function find(model, id, callback) {
callback(null, this.cache[model][id]);
process.nextTick(function () {
callback(null, this.cache[model][id]);
}.bind(this));
};

Memory.prototype.destroy = function destroy(model, id, callback) {
delete this.cache[model][id];
callback();
process.nextTick(callback);
};

Memory.prototype.all = function all(model, filter, callback) {
Expand Down Expand Up @@ -132,7 +140,7 @@ Memory.prototype.destroyAll = function destroyAll(model, callback) {
delete this.cache[model][id];
}.bind(this));
this.cache[model] = {};
callback();
process.nextTick(callback);
};

Memory.prototype.count = function count(model, callback, where) {
Expand All @@ -149,7 +157,9 @@ Memory.prototype.count = function count(model, callback, where) {
return ok;
});
}
callback(null, data.length);
process.nextTick(function () {
callback(null, data.length);
});
};

Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
Expand Down
Loading

0 comments on commit 61506ba

Please sign in to comment.