Promisify model relation methods

When a callback is omitted from a method on a model relation that
supports promises, return that promise.  This includes all the standard
DAO methods, as well as any user-defined methods that return promises.

e.g.:
mylist.todos.create({name: 'Item 1'}) // returns Promise

This API will use native ES6 promises if available.  If not available,
or to force the use of another Promise library, you must assign the
global.Promise object.

e.g.:
global.Promise = require('bluebird')

Relations affected:

- BelongsTo
- HasOne
- HasMany
- HasManyThrough
- HasAndBelongsToMany
- ReferencesMany
- EmbedsOne

Exceptions:

The EmbedsMany relation has not been promisified, because most of the
methods return synchronous values.

The base relation getter method [e.g.: mylist.todos()] has not been
promisified, due to its default caching behavior.

New Methods:

- getAsync(condition, cb)

A new method "getAsync()" has been added to all relations except
EmbedsMany, which always fetches from the datasource rather than from
the cache.  It takes an optional "where" condition (except for HasOne
and BelongsTo) and an optional callback.  If the callback is omitted,
a Promise is returned.
This commit is contained in:
Partap Davis 2015-02-17 22:04:31 -07:00 committed by Miroslav Bajtoš
parent 16cc870f57
commit 29eb3434c7
3 changed files with 1511 additions and 20 deletions

View File

@ -758,9 +758,12 @@ HasMany.prototype.findById = function (fkId, cb) {
filter.where[idName] = fkId;
filter.where[fk] = modelInstance[pk];
cb = cb || utils.createPromiseCallback();
if (filter.where[fk] === undefined) {
// Foreign key is undefined
return process.nextTick(cb);
process.nextTick(cb);
return cb.promise;
}
this.definition.applyScope(modelInstance, filter);
@ -784,6 +787,7 @@ HasMany.prototype.findById = function (fkId, cb) {
cb(err);
}
});
return cb.promise;
};
/**
@ -795,6 +799,8 @@ HasMany.prototype.exists = function (fkId, cb) {
var fk = this.definition.keyTo;
var pk = this.definition.keyFrom;
var modelInstance = this.modelInstance;
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function (err, inst) {
if (err) {
@ -810,6 +816,7 @@ HasMany.prototype.exists = function (fkId, cb) {
cb(null, false);
}
});
return cb.promise;
};
/**
@ -818,12 +825,14 @@ HasMany.prototype.exists = function (fkId, cb) {
* @param {Function} cb The callback function
*/
HasMany.prototype.updateById = function (fkId, data, cb) {
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function (err, inst) {
if (err) {
return cb && cb(err);
}
inst.updateAttributes(data, cb);
});
return cb.promise;
};
/**
@ -832,6 +841,7 @@ HasMany.prototype.updateById = function (fkId, data, cb) {
* @param {Function} cb The callback function
*/
HasMany.prototype.destroyById = function (fkId, cb) {
cb = cb || utils.createPromiseCallback();
var self = this;
this.findById(fkId, function(err, inst) {
if (err) {
@ -840,6 +850,7 @@ HasMany.prototype.destroyById = function (fkId, cb) {
self.removeFromCache(inst[fkId]);
inst.destroy(cb);
});
return cb.promise;
};
var throughKeys = function(definition) {
@ -875,6 +886,8 @@ HasManyThrough.prototype.findById = function (fkId, cb) {
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
@ -897,6 +910,7 @@ HasManyThrough.prototype.findById = function (fkId, cb) {
cb(err, inst);
});
});
return cb.promise;
};
/**
@ -911,6 +925,8 @@ HasManyThrough.prototype.destroyById = function (fkId, cb) {
var modelInstance = this.modelInstance;
var modelThrough = this.definition.modelThrough;
cb = cb || utils.createPromiseCallback();
self.exists(fkId, function (err, exists) {
if (err || !exists) {
if (!err) {
@ -928,6 +944,7 @@ HasManyThrough.prototype.destroyById = function (fkId, cb) {
modelTo.deleteById(fkId, cb);
});
});
return cb.promise;
};
// Create an instance of the target model and connect it to the instance of
@ -942,7 +959,7 @@ HasManyThrough.prototype.create = function create(data, done) {
done = data;
data = {};
}
done = done || function(){};
done = done || utils.createPromiseCallback();
var modelInstance = this.modelInstance;
@ -984,6 +1001,7 @@ HasManyThrough.prototype.create = function create(data, done) {
else
async.map(to, createRelation, done);
});
return done.promise;
};
@ -1005,6 +1023,9 @@ HasManyThrough.prototype.add = function (acInst, data, done) {
}
var query = {};
data = data || {};
done = done || utils.createPromiseCallback();
// The primary key for the target model
var pk2 = definition.modelTo.definition.idName();
@ -1033,6 +1054,7 @@ HasManyThrough.prototype.add = function (acInst, data, done) {
}
done(err, ac);
});
return done.promise;
};
/**
@ -1060,9 +1082,12 @@ HasManyThrough.prototype.exists = function (acInst, done) {
definition.applyScope(this.modelInstance, filter);
done = done || utils.createPromiseCallback();
modelThrough.count(filter.where, function(err, ac) {
done(err, ac > 0);
});
return done.promise;
};
/**
@ -1091,12 +1116,15 @@ HasManyThrough.prototype.remove = function (acInst, done) {
definition.applyScope(this.modelInstance, filter);
done = done || utils.createPromiseCallback();
modelThrough.deleteAll(filter.where, function (err) {
if (!err) {
self.removeFromCache(query[fk2]);
}
done(err);
});
return done.promise;
};
@ -1186,6 +1214,7 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
get: function() {
var relation = new BelongsTo(definition, this);
var relationMethod = relation.related.bind(relation);
relationMethod.getAsync = relation.getAsync.bind(relation);
relationMethod.update = relation.update.bind(relation);
relationMethod.destroy = relation.destroy.bind(relation);
if (!polymorphic) {
@ -1221,6 +1250,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) {
cb = targetModelData;
targetModelData = {};
}
cb = cb || utils.createPromiseCallback();
this.definition.applyProperties(modelInstance, targetModelData || {});
@ -1241,6 +1271,7 @@ BelongsTo.prototype.create = function(targetModelData, cb) {
cb && cb(err);
}
});
return cb.promise;
};
BelongsTo.prototype.build = function(targetModelData) {
@ -1250,6 +1281,7 @@ BelongsTo.prototype.build = function(targetModelData) {
};
BelongsTo.prototype.update = function (targetModelData, cb) {
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
this.fetch(function(err, inst) {
if (inst instanceof ModelBaseClass) {
@ -1259,12 +1291,16 @@ BelongsTo.prototype.update = function (targetModelData, cb) {
+ ' is empty'));
}
});
return cb.promise;
};
BelongsTo.prototype.destroy = function (cb) {
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var fk = this.definition.keyFrom;
cb = cb || utils.createPromiseCallback();
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
modelInstance[fk] = null;
@ -1277,6 +1313,7 @@ BelongsTo.prototype.destroy = function (cb) {
+ ' is empty'));
}
});
return cb.promise;
};
/**
@ -1394,6 +1431,21 @@ BelongsTo.prototype.related = function (refresh, params) {
}
};
/**
* Define a Promise-based method for the belongsTo relation itself
* - order.customer.get(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} returns promise if callback is omitted
*/
BelongsTo.prototype.getAsync = function (cb) {
cb = cb || utils.createPromiseCallback();
this.related(true, cb);
return cb.promise;
}
/**
* A hasAndBelongsToMany relation creates a direct many-to-many connection with
* another model, with no intervening model. For example, if your application
@ -1504,6 +1556,7 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) {
get: function() {
var relation = new HasOne(definition, this);
var relationMethod = relation.related.bind(relation)
relationMethod.getAsync = relation.getAsync.bind(relation);
relationMethod.create = relation.create.bind(relation);
relationMethod.build = relation.build.bind(relation);
relationMethod.update = relation.update.bind(relation);
@ -1559,6 +1612,8 @@ HasOne.prototype.create = function (targetModelData, cb) {
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
targetModelData[fk] = modelInstance[pk];
var query = {where: {}};
query.where[fk] = targetModelData[fk];
@ -1579,9 +1634,11 @@ HasOne.prototype.create = function (targetModelData, cb) {
+ modelTo.modelName));
}
});
return cb.promise;
};
HasOne.prototype.update = function(targetModelData, cb) {
cb = cb || utils.createPromiseCallback();
var definition = this.definition;
var fk = this.definition.keyTo;
this.fetch(function(err, targetModel) {
@ -1593,9 +1650,11 @@ HasOne.prototype.update = function(targetModelData, cb) {
+ ' is empty'));
}
});
return cb.promise;
};
HasOne.prototype.destroy = function (cb) {
cb = cb || utils.createPromiseCallback();
this.fetch(function(err, targetModel) {
if (targetModel instanceof ModelBaseClass) {
targetModel.destroy(cb);
@ -1604,6 +1663,7 @@ HasOne.prototype.destroy = function (cb) {
+ ' is empty'));
}
});
return cb.promise;
};
/**
@ -1625,6 +1685,7 @@ HasMany.prototype.create = function (targetModelData, cb) {
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var fkAndProps = function(item) {
item[fk] = modelInstance[pk];
@ -1650,6 +1711,7 @@ HasMany.prototype.create = function (targetModelData, cb) {
cb && cb(err);
}
});
return cb.promise;
};
/**
* Build a target model instance
@ -1741,6 +1803,20 @@ HasOne.prototype.related = function (refresh, params) {
}
};
/**
* Define a Promise-based method for the hasOne relation itself
* - order.customer.getAsync(cb): Load the target model instance asynchronously
*
* @param {Function} cb Callback of the form function (err, inst)
* @returns {Promise | Undefined} Returns promise if cb is omitted
*/
HasOne.prototype.getAsync = function (cb) {
cb = cb || utils.createPromiseCallback();
this.related(true, cb);
return cb.promise;
};
RelationDefinition.embedsOne = function (modelFrom, modelTo, params) {
params = params || {};
modelTo = lookupModelTo(modelFrom, modelTo, params);
@ -1896,6 +1972,7 @@ EmbedsOne.prototype.create = function (targetModelData, cb) {
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var inst = this.callScopeMethod('build', targetModelData);
@ -1928,6 +2005,7 @@ EmbedsOne.prototype.create = function (targetModelData, cb) {
updateEmbedded();
}
}
return cb.promise;
};
EmbedsOne.prototype.build = function (targetModelData) {
@ -1970,25 +2048,29 @@ EmbedsOne.prototype.update = function (targetModelData, cb) {
var embeddedInstance = modelInstance[propertyName];
if (embeddedInstance instanceof modelTo) {
embeddedInstance.setAttributes(data);
cb = cb || utils.createPromiseCallback();
if (typeof cb === 'function') {
modelInstance.save(function(err, inst) {
cb(err, inst ? inst[propertyName] : embeddedInstance);
});
}
} else if (!embeddedInstance && cb) {
this.callScopeMethod('create', data, cb);
return this.callScopeMethod('create', data, cb);
} else if (!embeddedInstance) {
this.callScopeMethod('build', data);
return this.callScopeMethod('build', data);
}
return cb.promise;
};
EmbedsOne.prototype.destroy = function (cb) {
var modelInstance = this.modelInstance;
var propertyName = this.definition.keyFrom;
modelInstance.unsetAttribute(propertyName, true);
cb = cb || utils.createPromiseCallback();
modelInstance.save(function (err, result) {
cb && cb(err, result);
});
return cb.promise;
};
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) {
@ -2386,6 +2468,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) {
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var embeddedList = this.embeddedList();
@ -2420,6 +2503,7 @@ EmbedsMany.prototype.create = function (targetModelData, cb) {
updateEmbedded();
}
}
return cb.promise;
};
EmbedsMany.prototype.build = function(targetModelData) {
@ -2478,6 +2562,7 @@ EmbedsMany.prototype.add = function (acInst, data, cb) {
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
var self = this;
var definition = this.definition;
@ -2513,6 +2598,7 @@ EmbedsMany.prototype.add = function (acInst, data, cb) {
cb(null, null);
}
});
return cb.promise;
};
/**
@ -2543,6 +2629,8 @@ EmbedsMany.prototype.remove = function (acInst, cb) {
belongsTo.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelInstance[definition.name](filter, function(err, items) {
if (err) return cb(err);
@ -2554,6 +2642,7 @@ EmbedsMany.prototype.remove = function (acInst, cb) {
cb(err);
});
});
return cb.promise;
};
RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, params) {
@ -2654,6 +2743,7 @@ ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh
var actualCond = {};
var actualRefresh = false;
if (arguments.length === 3) {
cb = condOrRefresh;
} else if (arguments.length === 4) {
@ -2696,6 +2786,8 @@ ReferencesMany.prototype.findById = function (fkId, cb) {
this.definition.applyScope(modelInstance, filter);
cb = cb || utils.createPromiseCallback();
modelTo.findByIds(ids, filter, function (err, instances) {
if (err) {
return cb(err);
@ -2722,6 +2814,7 @@ ReferencesMany.prototype.findById = function (fkId, cb) {
cb(err);
}
});
return cb.promise;
};
ReferencesMany.prototype.exists = function (fkId, cb) {
@ -2729,7 +2822,10 @@ ReferencesMany.prototype.exists = function (fkId, cb) {
var ids = this.modelInstance[fk] || [];
var currentIds = ids.map(function(id) { return id.toString(); });
var fkId = (fkId || '').toString(); // mongodb
cb = cb || utils.createPromiseCallback();
process.nextTick(function() { cb(null, currentIds.indexOf(fkId) > -1) });
return cb.promise;
};
ReferencesMany.prototype.updateById = function (fkId, data, cb) {
@ -2737,27 +2833,33 @@ ReferencesMany.prototype.updateById = function (fkId, data, cb) {
cb = data;
data = {};
}
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function(err, inst) {
if (err) return cb(err);
inst.updateAttributes(data, cb);
});
return cb.promise;
};
ReferencesMany.prototype.destroyById = function (fkId, cb) {
var self = this;
cb = cb || utils.createPromiseCallback();
this.findById(fkId, function(err, inst) {
if (err) return cb(err);
self.remove(inst, function(err, ids) {
inst.destroy(cb);
});
});
return cb.promise;
};
ReferencesMany.prototype.at = function (index, cb) {
var fk = this.definition.keyFrom;
var ids = this.modelInstance[fk] || [];
cb = cb || utils.createPromiseCallback();
this.findById(ids[index], cb);
return cb.promise;
};
ReferencesMany.prototype.create = function (targetModelData, cb) {
@ -2774,6 +2876,7 @@ ReferencesMany.prototype.create = function (targetModelData, cb) {
targetModelData = {};
}
targetModelData = targetModelData || {};
cb = cb || utils.createPromiseCallback();
var ids = modelInstance[fk] || [];
@ -2799,6 +2902,7 @@ ReferencesMany.prototype.create = function (targetModelData, cb) {
cb(err, inst);
});
});
return cb.promise;
};
ReferencesMany.prototype.build = function(targetModelData) {
@ -2843,6 +2947,8 @@ ReferencesMany.prototype.add = function (acInst, cb) {
});
};
cb = cb || utils.createPromiseCallback();
if (acInst instanceof modelTo) {
insert(acInst, cb);
} else {
@ -2856,6 +2962,7 @@ ReferencesMany.prototype.add = function (acInst, cb) {
insert(inst, cb);
});
}
return cb.promise;
};
/**
@ -2876,6 +2983,8 @@ ReferencesMany.prototype.remove = function (acInst, cb) {
var id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
id = id.toString();
cb = cb || utils.createPromiseCallback();
var index = currentIds.indexOf(id);
if (index > -1) {
ids.splice(index, 1);
@ -2885,4 +2994,5 @@ ReferencesMany.prototype.remove = function (acInst, cb) {
} else {
process.nextTick(function() { cb(null, ids); });
}
return cb.promise;
};

View File

@ -39,7 +39,7 @@ ScopeDefinition.prototype.targetModel = function(receiver) {
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
var name = this.name;
var self = receiver;
var actualCond = {};
var actualRefresh = false;
var saveOnCache = true;
@ -56,7 +56,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
} else {
throw new Error('Method can be only called with one or two arguments');
}
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|| actualRefresh) {
// It either doesn't hit the cache or refresh is required
@ -148,7 +148,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
get: function () {
var targetModel = definition.targetModel(this);
var self = this;
var f = function(condOrRefresh, cb) {
if (arguments.length === 0) {
if (typeof f.value === 'function') {
@ -176,16 +176,35 @@ function defineScope(cls, targetClass, name, params, methods, options) {
}
}
};
f._receiver = this;
f._scope = typeof definition.params === 'function' ?
definition.params.call(self) : definition.params;
f._targetClass = targetModel.modelName;
if (f._scope.collect) {
f._targetClass = i8n.camelize(f._scope.collect);
}
f.getAsync = function (cond, cb) {
if (cb === undefined) {
if (cond === undefined) {
// getAsync()
cb = utils.createPromiseCallback();
cond = true;
} else if (typeof cond !== 'function') {
// getAsync({where:{}})
cb = utils.createPromiseCallback();
} else {
// getAsync(function(){})
cb = cond;
cond = true;
}
}
definition.related(self, f._scope, cond, cb);
return cb.promise;
}
f.build = build;
f.create = create;
f.updateAll = updateAll;
@ -261,7 +280,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
};
cls['__findOne__' + name] = fn_findOne;
var fn_count = function (cb) {
var f = this[name].count;
f.apply(this[name], arguments);
@ -284,9 +303,10 @@ function defineScope(cls, targetClass, name, params, methods, options) {
cb = data;
data = {};
}
this.build(data).save(cb);
cb = cb || utils.createPromiseCallback();
return this.build(data).save(cb);
}
/*
Callback
- The callback will be called after all elements are destroyed
@ -295,10 +315,12 @@ function defineScope(cls, targetClass, name, params, methods, options) {
*/
function destroyAll(where, cb) {
if (typeof where === 'function') cb = where, where = {};
cb = cb || utils.createPromiseCallback();
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetModel.destroyAll(filter.where, cb);
return targetModel.destroyAll(filter.where, cb);
}
function updateAll(where, data, cb) {
@ -332,11 +354,13 @@ function defineScope(cls, targetClass, name, params, methods, options) {
function count(where, cb) {
if (typeof where === 'function') cb = where, where = {};
cb = cb || utils.createPromiseCallback();
var targetModel = definition.targetModel(this._receiver);
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetModel.count(filter.where, cb);
}
return targetModel.count(filter.where, cb);
}
return definition;
}

File diff suppressed because it is too large Load Diff