Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
ba68df3ef3
12
CHANGES.md
12
CHANGES.md
|
@ -1,3 +1,15 @@
|
|||
2015-05-13, Version 2.27.0
|
||||
==========================
|
||||
|
||||
* Fix the target id resolution (Raymond Feng)
|
||||
|
||||
* DB Call Optimization in relation includes - Fixes #408 & #166 (ningsuhen)
|
||||
|
||||
* Conditionally pass options to connector CRUD methods (Raymond Feng)
|
||||
|
||||
* Pass-through options from save to create (Fabien Franzen)
|
||||
|
||||
|
||||
2015-05-05, Version 2.26.4
|
||||
==========================
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ Memory.prototype.define = function defineModel(definition) {
|
|||
if(!this.collection(m)) this.initCollection(m);
|
||||
};
|
||||
|
||||
Memory.prototype.create = function create(model, data, callback) {
|
||||
Memory.prototype.create = function create(model, data, options, callback) {
|
||||
// FIXME: [rfeng] We need to generate unique ids based on the id type
|
||||
// FIXME: [rfeng] We don't support composite ids yet
|
||||
var currentId = this.collectionSeq(model);
|
||||
|
@ -221,15 +221,15 @@ Memory.prototype.create = function create(model, data, callback) {
|
|||
this.saveToFile(id, callback);
|
||||
};
|
||||
|
||||
Memory.prototype.updateOrCreate = function (model, data, callback) {
|
||||
Memory.prototype.updateOrCreate = function (model, data, options, callback) {
|
||||
var self = this;
|
||||
this.exists(model, self.getIdValue(model, data), function (err, exists) {
|
||||
this.exists(model, self.getIdValue(model, data), options, function (err, exists) {
|
||||
if (exists) {
|
||||
self.save(model, data, function(err, data) {
|
||||
self.save(model, data, options, function(err, data) {
|
||||
callback(err, data, { isNewInstance: false });
|
||||
});
|
||||
} else {
|
||||
self.create(model, data, function (err, id) {
|
||||
self.create(model, data, options, function (err, id) {
|
||||
self.setIdValue(model, data, id);
|
||||
callback(err, data, { isNewInstance: true });
|
||||
});
|
||||
|
@ -237,7 +237,7 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
|
|||
});
|
||||
};
|
||||
|
||||
Memory.prototype.save = function save(model, data, callback) {
|
||||
Memory.prototype.save = function save(model, data, options, callback) {
|
||||
var id = this.getIdValue(model, data);
|
||||
var cachedModels = this.collection(model);
|
||||
var modelData = cachedModels && this.collection(model)[id];
|
||||
|
@ -251,19 +251,19 @@ Memory.prototype.save = function save(model, data, callback) {
|
|||
});
|
||||
};
|
||||
|
||||
Memory.prototype.exists = function exists(model, id, callback) {
|
||||
Memory.prototype.exists = function exists(model, id, options, callback) {
|
||||
process.nextTick(function () {
|
||||
callback(null, this.collection(model) && this.collection(model).hasOwnProperty(id));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Memory.prototype.find = function find(model, id, callback) {
|
||||
Memory.prototype.find = function find(model, id, options, callback) {
|
||||
process.nextTick(function () {
|
||||
callback(null, id in this.collection(model) && this.fromDb(model, this.collection(model)[id]));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
Memory.prototype.destroy = function destroy(model, id, callback) {
|
||||
Memory.prototype.destroy = function destroy(model, id, options, callback) {
|
||||
delete this.collection(model)[id];
|
||||
this.saveToFile(null, callback);
|
||||
};
|
||||
|
@ -310,7 +310,7 @@ function getValue(obj, path) {
|
|||
return val;
|
||||
}
|
||||
|
||||
Memory.prototype.all = function all(model, filter, callback) {
|
||||
Memory.prototype.all = function all(model, filter, options, callback) {
|
||||
var self = this;
|
||||
var nodes = Object.keys(this.collection(model)).map(function (key) {
|
||||
return this.fromDb(model, this.collection(model)[key]);
|
||||
|
@ -559,11 +559,7 @@ function applyFilter(filter) {
|
|||
}
|
||||
}
|
||||
|
||||
Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
|
||||
if (!callback && 'function' === typeof where) {
|
||||
callback = where;
|
||||
where = undefined;
|
||||
}
|
||||
Memory.prototype.destroyAll = function destroyAll(model, where, options, callback) {
|
||||
var cache = this.collection(model);
|
||||
var filter = null;
|
||||
var count = 0;
|
||||
|
@ -582,7 +578,7 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
|
|||
this.saveToFile({ count: count }, callback);
|
||||
};
|
||||
|
||||
Memory.prototype.count = function count(model, callback, where) {
|
||||
Memory.prototype.count = function count(model, where, options, callback) {
|
||||
var cache = this.collection(model);
|
||||
var data = Object.keys(cache);
|
||||
if (where) {
|
||||
|
@ -598,7 +594,7 @@ Memory.prototype.count = function count(model, callback, where) {
|
|||
};
|
||||
|
||||
Memory.prototype.update =
|
||||
Memory.prototype.updateAll = function updateAll(model, where, data, cb) {
|
||||
Memory.prototype.updateAll = function updateAll(model, where, data, options, cb) {
|
||||
var self = this;
|
||||
var cache = this.collection(model);
|
||||
var filter = null;
|
||||
|
@ -614,7 +610,7 @@ Memory.prototype.update =
|
|||
// The id value from the cache is string
|
||||
// Get the real id from the inst
|
||||
id = self.getIdValue(model, inst);
|
||||
self.updateAttributes(model, id, data, done);
|
||||
self.updateAttributes(model, id, data, options, done);
|
||||
} else {
|
||||
process.nextTick(done);
|
||||
}
|
||||
|
@ -624,7 +620,7 @@ Memory.prototype.update =
|
|||
});
|
||||
};
|
||||
|
||||
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
|
||||
Memory.prototype.updateAttributes = function updateAttributes(model, id, data, options, cb) {
|
||||
if (!id) {
|
||||
var err = new Error('You must provide an id when updating attributes!');
|
||||
if (cb) {
|
||||
|
@ -643,7 +639,7 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
|
|||
var modelData = cachedModels && this.collection(model)[id];
|
||||
|
||||
if (modelData) {
|
||||
this.save(model, data, cb);
|
||||
this.save(model, data, options, cb);
|
||||
} else {
|
||||
cb(new Error('Could not update attributes. Object with id ' + id + ' does not exist!'));
|
||||
}
|
||||
|
|
253
lib/dao.js
253
lib/dao.js
|
@ -124,6 +124,14 @@ DataAccessObject.lookupModel = function(data) {
|
|||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the connector instance for the given model class
|
||||
* @returns {Connector} The connector instance
|
||||
*/
|
||||
DataAccessObject.getConnector = function() {
|
||||
return this.getDataSource().connector;
|
||||
}
|
||||
|
||||
// Empty callback function
|
||||
function noCallback(err, result) {
|
||||
// NOOP
|
||||
|
@ -154,6 +162,10 @@ DataAccessObject.create = function (data, options, cb) {
|
|||
}
|
||||
|
||||
var Model = this;
|
||||
var connector = Model.getConnector();
|
||||
assert(typeof connector.create === 'function',
|
||||
'create() must be implemented by the connector');
|
||||
|
||||
var self = this;
|
||||
|
||||
if (options === undefined && cb === undefined) {
|
||||
|
@ -260,7 +272,7 @@ DataAccessObject.create = function (data, options, cb) {
|
|||
var _idName = idName(Model);
|
||||
var modelName = Model.modelName;
|
||||
var val = removeUndefined(obj.toObject(true));
|
||||
this._adapter().create(modelName, this.constructor._forDB(val), function (err, id, rev) {
|
||||
function createCallback(err, id, rev) {
|
||||
if (id) {
|
||||
obj.__data[_idName] = id;
|
||||
defineReadonlyProp(obj, _idName, id);
|
||||
|
@ -290,7 +302,13 @@ DataAccessObject.create = function (data, options, cb) {
|
|||
});
|
||||
});
|
||||
});
|
||||
}, obj);
|
||||
}
|
||||
|
||||
if (connector.create.length === 4) {
|
||||
connector.create(modelName, this.constructor._forDB(val), options, createCallback);
|
||||
} else {
|
||||
connector.create(modelName, this.constructor._forDB(val), createCallback);
|
||||
}
|
||||
}, obj, cb);
|
||||
}, obj, cb);
|
||||
}
|
||||
|
@ -363,6 +381,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
|
||||
var self = this;
|
||||
var Model = this;
|
||||
var connector = Model.getConnector();
|
||||
|
||||
var id = getIdValue(this, data);
|
||||
if (id === undefined || id === null) {
|
||||
|
@ -381,7 +400,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
if (err) return cb(err);
|
||||
|
||||
var isOriginalQuery = isWhereByGivenId(Model, ctx.query.where, id)
|
||||
if (Model.getDataSource().connector.updateOrCreate && isOriginalQuery) {
|
||||
if (connector.updateOrCreate && isOriginalQuery) {
|
||||
var context = {
|
||||
Model: Model,
|
||||
where: ctx.query.where,
|
||||
|
@ -403,10 +422,15 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
Model.applyProperties(update, inst);
|
||||
Model = Model.lookupModel(update);
|
||||
|
||||
var connector = self.getConnector();
|
||||
|
||||
if (Model.settings.validateUpsert === false) {
|
||||
update = removeUndefined(update);
|
||||
self.getDataSource().connector
|
||||
.updateOrCreate(Model.modelName, update, done);
|
||||
if (connector.updateOrCreate.length === 4) {
|
||||
connector.updateOrCreate(Model.modelName, update, options, done);
|
||||
} else {
|
||||
connector.updateOrCreate(Model.modelName, update, done);
|
||||
}
|
||||
} else {
|
||||
inst.isValid(function(valid) {
|
||||
if (!valid) {
|
||||
|
@ -421,8 +445,11 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
}
|
||||
|
||||
update = removeUndefined(update);
|
||||
self.getDataSource().connector
|
||||
.updateOrCreate(Model.modelName, update, done);
|
||||
if (connector.updateOrCreate.length === 4) {
|
||||
connector.updateOrCreate(Model.modelName, update, options, done);
|
||||
} else {
|
||||
connector.updateOrCreate(Model.modelName, update, done);
|
||||
}
|
||||
}, update);
|
||||
}
|
||||
|
||||
|
@ -533,47 +560,52 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb)
|
|||
|
||||
var Model = this;
|
||||
var self = this;
|
||||
var connector = Model.getConnector();
|
||||
|
||||
function _findOrCreate(query, data) {
|
||||
var modelName = self.modelName;
|
||||
data = removeUndefined(data);
|
||||
self.getDataSource().connector.findOrCreate(modelName, query,
|
||||
self._forDB(data),
|
||||
function(err, data, created) {
|
||||
var obj, Model = self.lookupModel(data);
|
||||
function findOrCreateCallback(err, data, created) {
|
||||
var obj, Model = self.lookupModel(data);
|
||||
|
||||
if (data) {
|
||||
obj = new Model(data, {fields: query.fields, applySetters: false,
|
||||
persisted: true});
|
||||
}
|
||||
if (data) {
|
||||
obj = new Model(data, {fields: query.fields, applySetters: false,
|
||||
persisted: true});
|
||||
}
|
||||
|
||||
if (created) {
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: obj,
|
||||
isNewInstance: true,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('after save', context, function(err) {
|
||||
if (cb.promise) {
|
||||
cb(err, [obj, created]);
|
||||
} else {
|
||||
cb(err, obj, created);
|
||||
}
|
||||
if (!err) Model.emit('changed', obj);
|
||||
});
|
||||
} else {
|
||||
if (created) {
|
||||
var context = {
|
||||
Model: Model,
|
||||
instance: obj,
|
||||
isNewInstance: true,
|
||||
hookState: hookState,
|
||||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('after save', context, function(err) {
|
||||
if (cb.promise) {
|
||||
cb(err, [obj, created]);
|
||||
} else {
|
||||
cb(err, obj, created);
|
||||
}
|
||||
if (!err) Model.emit('changed', obj);
|
||||
});
|
||||
} else {
|
||||
if (cb.promise) {
|
||||
cb(err, [obj, created]);
|
||||
} else {
|
||||
cb(err, obj, created);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (connector.findOrCreate.length === 5) {
|
||||
connector.findOrCreate(modelName, query, self._forDB(data), options, findOrCreateCallback);
|
||||
} else {
|
||||
connector.findOrCreate(modelName, query, self._forDB(data), findOrCreateCallback);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getDataSource().connector.findOrCreate) {
|
||||
if (connector.findOrCreate) {
|
||||
query.limit = 1;
|
||||
|
||||
try {
|
||||
|
@ -707,7 +739,7 @@ DataAccessObject.exists = function exists(id, options, cb) {
|
|||
* @param {Object} [options] Options
|
||||
* @param {Function} cb Callback called with (err, instance)
|
||||
*/
|
||||
DataAccessObject.findById = function find(id, filter, options, cb) {
|
||||
DataAccessObject.findById = function findById(id, filter, options, cb) {
|
||||
var connectionPromise = stillConnecting(this.getDataSource(), this, arguments);
|
||||
if (connectionPromise) {
|
||||
return connectionPromise;
|
||||
|
@ -1179,6 +1211,10 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
|
||||
var hookState = {};
|
||||
var self = this;
|
||||
var connector = self.getConnector();
|
||||
|
||||
assert(typeof connector.all === 'function',
|
||||
'all() must be implemented by the connector');
|
||||
|
||||
try {
|
||||
this._normalize(query);
|
||||
|
@ -1192,12 +1228,12 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
this.applyScope(query);
|
||||
|
||||
var near = query && geo.nearFilter(query.where);
|
||||
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
||||
var supportsGeo = !!connector.buildNearFilter;
|
||||
|
||||
if (near) {
|
||||
if (supportsGeo) {
|
||||
// convert it
|
||||
this.getDataSource().connector.buildNearFilter(query, near);
|
||||
connector.buildNearFilter(query, near);
|
||||
} else if (query.where) {
|
||||
// do in memory query
|
||||
// using all documents
|
||||
|
@ -1212,7 +1248,7 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
self.notifyObserversOf('access', context, function(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.getDataSource().connector.all(self.modelName, {}, function (err, data) {
|
||||
function geoCallback(err, data) {
|
||||
var memory = new Memory();
|
||||
var modelName = self.modelName;
|
||||
|
||||
|
@ -1225,18 +1261,24 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
model: self
|
||||
});
|
||||
|
||||
data.forEach(function (obj) {
|
||||
memory.create(modelName, obj, function () {
|
||||
data.forEach(function(obj) {
|
||||
memory.create(modelName, obj, options, function() {
|
||||
// noop
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME: apply "includes" and other transforms - see allCb below
|
||||
memory.all(modelName, ctx.query, cb);
|
||||
memory.all(modelName, ctx.query, options, cb);
|
||||
} else {
|
||||
cb(null, []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (connector.all.length === 4) {
|
||||
connector.all(self.modelName, {}, options, geoCallback);
|
||||
} else {
|
||||
connector.all(self.modelName, {}, geoCallback);
|
||||
}
|
||||
});
|
||||
|
||||
// already handled
|
||||
|
@ -1301,7 +1343,11 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
};
|
||||
|
||||
if (options.notify === false) {
|
||||
self.getDataSource().connector.all(self.modelName, query, allCb);
|
||||
if (connector.all.length === 4) {
|
||||
connector.all(self.modelName, query, options, allCb);
|
||||
} else {
|
||||
connector.all(self.modelName, query, allCb);
|
||||
}
|
||||
} else {
|
||||
var context = {
|
||||
Model: this,
|
||||
|
@ -1312,7 +1358,11 @@ DataAccessObject.find = function find(query, options, cb) {
|
|||
this.notifyObserversOf('access', context, function(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
var query = ctx.query;
|
||||
self.getDataSource().connector.all(self.modelName, query, allCb);
|
||||
if (connector.all.length === 4) {
|
||||
connector.all(self.modelName, query, options, allCb);
|
||||
} else {
|
||||
connector.all(self.modelName, query, allCb);
|
||||
}
|
||||
});
|
||||
}
|
||||
return cb.promise;
|
||||
|
@ -1381,6 +1431,10 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
}
|
||||
|
||||
var Model = this;
|
||||
var connector = Model.getConnector();
|
||||
|
||||
assert(typeof connector.destroyAll === 'function',
|
||||
'destroyAll() must be implemented by the connector');
|
||||
|
||||
if (options === undefined && cb === undefined) {
|
||||
if (typeof where === 'function') {
|
||||
|
@ -1442,7 +1496,11 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
|
||||
function doDelete(where) {
|
||||
if (whereIsEmpty(where)) {
|
||||
Model.getDataSource().connector.destroyAll(Model.modelName, done);
|
||||
if (connector.destroyAll.length === 4) {
|
||||
connector.destroyAll(Model.modelName, {}, options, done);
|
||||
} else {
|
||||
connector.destroyAll(Model.modelName, {}, done);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// Support an optional where object
|
||||
|
@ -1454,7 +1512,11 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
});
|
||||
}
|
||||
|
||||
Model.getDataSource().connector.destroyAll(Model.modelName, where, done);
|
||||
if (connector.destroyAll.length === 4) {
|
||||
connector.destroyAll(Model.modelName, where, options, done);
|
||||
} else {
|
||||
connector.destroyAll(Model.modelName, where, done);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1577,6 +1639,13 @@ DataAccessObject.count = function (where, options, cb) {
|
|||
assert(typeof options === 'object', 'The options argument must be an object');
|
||||
assert(typeof cb === 'function', 'The cb argument must be a function');
|
||||
|
||||
var Model = this;
|
||||
var connector = Model.getConnector();
|
||||
assert(typeof connector.count === 'function',
|
||||
'count() must be implemented by the connector');
|
||||
assert(connector.count.length >= 3,
|
||||
'count() must take at least 3 arguments');
|
||||
|
||||
var hookState = {};
|
||||
|
||||
var query = { where: where };
|
||||
|
@ -1593,8 +1662,6 @@ DataAccessObject.count = function (where, options, cb) {
|
|||
return cb.promise;
|
||||
}
|
||||
|
||||
var Model = this;
|
||||
|
||||
var context = {
|
||||
Model: Model,
|
||||
query: { where: where },
|
||||
|
@ -1602,10 +1669,19 @@ DataAccessObject.count = function (where, options, cb) {
|
|||
options: options
|
||||
};
|
||||
this.notifyObserversOf('access', context, function(err, ctx) {
|
||||
if (err) return cb(err);
|
||||
where = ctx.query.where;
|
||||
Model.getDataSource().connector.count(Model.modelName, cb, where);
|
||||
});
|
||||
if (err) return cb(err);
|
||||
where = ctx.query.where;
|
||||
|
||||
if (connector.count.length <= 3) {
|
||||
// Old signature, please note where is the last
|
||||
// count(model, cb, where)
|
||||
connector.count(Model.modelName, cb, where);
|
||||
} else {
|
||||
// New signature
|
||||
// count(model, where, options, cb)
|
||||
connector.count(Model.modelName, where, options, cb);
|
||||
}
|
||||
});
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
|
@ -1649,6 +1725,7 @@ DataAccessObject.prototype.save = function (options, cb) {
|
|||
}
|
||||
|
||||
var inst = this;
|
||||
var connector = inst.getConnector();
|
||||
var modelName = Model.modelName;
|
||||
|
||||
var context = {
|
||||
|
@ -1687,7 +1764,7 @@ DataAccessObject.prototype.save = function (options, cb) {
|
|||
inst.trigger('save', function (saveDone) {
|
||||
inst.trigger('update', function (updateDone) {
|
||||
data = removeUndefined(data);
|
||||
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err, unusedData, result) {
|
||||
function saveCallback(err, unusedData, result) {
|
||||
if (err) {
|
||||
return cb(err, inst);
|
||||
}
|
||||
|
@ -1710,7 +1787,13 @@ DataAccessObject.prototype.save = function (options, cb) {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (connector.save.length === 4) {
|
||||
connector.save(modelName, inst.constructor._forDB(data), options, saveCallback);
|
||||
} else {
|
||||
connector.save(modelName, inst.constructor._forDB(data), saveCallback);
|
||||
}
|
||||
}, data, cb);
|
||||
}, data, cb);
|
||||
}
|
||||
|
@ -1775,6 +1858,11 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
|
|||
assert(typeof options === 'object', 'The options argument must be an object');
|
||||
assert(typeof cb === 'function', 'The cb argument must be a function');
|
||||
|
||||
var Model = this;
|
||||
var connector = Model.getDataSource().connector;
|
||||
assert(typeof connector.update === 'function',
|
||||
'update() must be implemented by the connector');
|
||||
|
||||
var hookState = {};
|
||||
|
||||
var query = { where: where };
|
||||
|
@ -1783,8 +1871,6 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
|
|||
|
||||
where = query.where;
|
||||
|
||||
var Model = this;
|
||||
|
||||
var context = {
|
||||
Model: Model,
|
||||
query: { where: where },
|
||||
|
@ -1807,7 +1893,6 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
function doUpdate(where, data) {
|
||||
try {
|
||||
where = removeUndefined(where);
|
||||
|
@ -1820,8 +1905,7 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
var connector = Model.getDataSource().connector;
|
||||
connector.update(Model.modelName, where, data, function(err, info) {
|
||||
function updateCallback(err, info) {
|
||||
if (err) return cb (err);
|
||||
var context = {
|
||||
Model: Model,
|
||||
|
@ -1831,9 +1915,15 @@ DataAccessObject.updateAll = function (where, data, options, cb) {
|
|||
options: options
|
||||
};
|
||||
Model.notifyObserversOf('after save', context, function(err, ctx) {
|
||||
return cb(err, info);
|
||||
});
|
||||
});
|
||||
return cb(err, info);
|
||||
});
|
||||
}
|
||||
|
||||
if (connector.update.length === 5) {
|
||||
connector.update(Model.modelName, where, data, options, updateCallback);
|
||||
} else {
|
||||
connector.update(Model.modelName, where, data, updateCallback);
|
||||
}
|
||||
}
|
||||
return cb.promise;
|
||||
};
|
||||
|
@ -1846,7 +1936,7 @@ DataAccessObject.prototype.isNewRecord = function () {
|
|||
* Return connector of current record
|
||||
* @private
|
||||
*/
|
||||
DataAccessObject.prototype._adapter = function () {
|
||||
DataAccessObject.prototype.getConnector = function () {
|
||||
return this.getDataSource().connector;
|
||||
};
|
||||
|
||||
|
@ -1877,11 +1967,12 @@ DataAccessObject.prototype.remove =
|
|||
assert(typeof options === 'object', 'The options argument should be an object');
|
||||
assert(typeof cb === 'function', 'The cb argument should be a function');
|
||||
|
||||
var hookState = {};
|
||||
|
||||
var inst = this;
|
||||
var connector = this.getConnector();
|
||||
|
||||
var Model = this.constructor;
|
||||
var id = getIdValue(this.constructor, this);
|
||||
var hookState = {};
|
||||
|
||||
var context = {
|
||||
Model: Model,
|
||||
|
@ -1928,12 +2019,12 @@ DataAccessObject.prototype.remove =
|
|||
}
|
||||
|
||||
inst.trigger('destroy', function (destroyed) {
|
||||
inst._adapter().destroy(inst.constructor.modelName, id, function (err) {
|
||||
function destroyCallback(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
destroyed(function () {
|
||||
destroyed(function() {
|
||||
var context = {
|
||||
Model: Model,
|
||||
where: where,
|
||||
|
@ -1946,7 +2037,13 @@ DataAccessObject.prototype.remove =
|
|||
if (!err) Model.emit('deleted', id);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (connector.destroy.length === 4) {
|
||||
connector.destroy(inst.constructor.modelName, id, options, destroyCallback);
|
||||
} else {
|
||||
connector.destroy(inst.constructor.modelName, id, destroyCallback);
|
||||
}
|
||||
}, null, cb);
|
||||
}
|
||||
return cb.promise;
|
||||
|
@ -2066,11 +2163,14 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
|
|||
assert(typeof options === 'object', 'The options argument must be an object');
|
||||
assert(typeof cb === 'function', 'The cb argument must be a function');
|
||||
|
||||
var hookState = {};
|
||||
|
||||
var inst = this;
|
||||
var Model = this.constructor;
|
||||
var connector = inst.getConnector();
|
||||
assert(typeof connector.updateAttributes === 'function',
|
||||
'updateAttributes() must be implemented by the connector');
|
||||
|
||||
var model = Model.modelName;
|
||||
var hookState = {};
|
||||
|
||||
// Convert the data to be plain object so that update won't be confused
|
||||
if (data instanceof Model) {
|
||||
|
@ -2129,8 +2229,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
|
|||
}
|
||||
}
|
||||
|
||||
inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst),
|
||||
inst.constructor._forDB(typedData), function (err) {
|
||||
function updateAttributesCallback(err) {
|
||||
if (!err) inst.__persisted = true;
|
||||
done.call(inst, function () {
|
||||
saveDone.call(inst, function () {
|
||||
|
@ -2148,7 +2247,15 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, op
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (connector.updateAttributes.length === 5) {
|
||||
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
|
||||
inst.constructor._forDB(typedData), options, updateAttributesCallback);
|
||||
} else {
|
||||
connector.updateAttributes(model, getIdValue(inst.constructor, inst),
|
||||
inst.constructor._forDB(typedData), updateAttributesCallback);
|
||||
}
|
||||
}, data, cb);
|
||||
}, data, cb);
|
||||
}, data);
|
||||
|
|
644
lib/include.js
644
lib/include.js
|
@ -2,6 +2,7 @@ var async = require('async');
|
|||
var utils = require('./utils');
|
||||
var isPlainObject = utils.isPlainObject;
|
||||
var defineCachedRelations = utils.defineCachedRelations;
|
||||
var debug = require('debug')('loopback:include');
|
||||
|
||||
/*!
|
||||
* Normalize the include to be an array
|
||||
|
@ -57,6 +58,52 @@ IncludeScope.prototype.include = function() {
|
|||
return this._include;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the idKey of a Model.
|
||||
* @param {ModelConstructor} m - Model Constructor
|
||||
* @returns {String}
|
||||
*/
|
||||
function idName(m) {
|
||||
return m.definition.idName() || 'id';
|
||||
}
|
||||
|
||||
/*!
|
||||
* Look up a model by name from the list of given models
|
||||
* @param {Object} models Models keyed by name
|
||||
* @param {String} modelName The model name
|
||||
* @returns {*} The matching model class
|
||||
*/
|
||||
function lookupModel(models, modelName) {
|
||||
if (models[modelName]) {
|
||||
return models[modelName];
|
||||
}
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Function to allow interleave before and after high computation tasks
|
||||
* @param tasks
|
||||
* @param callback
|
||||
*/
|
||||
function execTasksWithInterLeave(tasks, callback) {
|
||||
//let's give others some time to process.
|
||||
//Context Switch BEFORE Heavy Computation
|
||||
process.nextTick(function () {
|
||||
//Heavy Computation
|
||||
async.parallel(tasks, function (err, info) {
|
||||
//Context Switch AFTER Heavy Computation
|
||||
process.nextTick(function () {
|
||||
callback(err, info);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
* Include mixin for ./model.js
|
||||
*/
|
||||
|
@ -92,18 +139,19 @@ Inclusion.normalizeInclude = normalizeInclude;
|
|||
* `User.include(users, ['posts', 'passports'], function() {});`
|
||||
*
|
||||
* Load all passports owner (users), and all posts of each owner loaded:
|
||||
*```Passport.include(passports, {owner: 'posts'}, function() {});
|
||||
*``` Passport.include(passports, {owner: ['posts', 'passports']});
|
||||
*``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
|
||||
*```Passport.include(passports, {owner: 'posts'}, function() {});
|
||||
*``` Passport.include(passports, {owner: ['posts', 'passports']});
|
||||
*``` Passport.include(passports, {owner: [{posts: 'images'}, 'passports']});
|
||||
*
|
||||
* @param {Array} objects Array of instances
|
||||
* @param {String|Object|Array} include Which relations to load.
|
||||
* @param {Function} cb Callback called when relations are loaded
|
||||
*
|
||||
*
|
||||
*/
|
||||
Inclusion.include = function (objects, include, cb) {
|
||||
debug('include', include);
|
||||
var self = this;
|
||||
|
||||
|
||||
if (!include || (Array.isArray(include) && include.length === 0) ||
|
||||
(isPlainObject(include) && Object.keys(include).length === 0)) {
|
||||
// The objects are empty
|
||||
|
@ -113,7 +161,7 @@ Inclusion.include = function (objects, include, cb) {
|
|||
}
|
||||
|
||||
include = normalizeInclude(include);
|
||||
|
||||
|
||||
async.each(include, function(item, callback) {
|
||||
processIncludeItem(objects, item, callback);
|
||||
}, function(err) {
|
||||
|
@ -122,10 +170,10 @@ Inclusion.include = function (objects, include, cb) {
|
|||
|
||||
function processIncludeItem(objs, include, cb) {
|
||||
var relations = self.relations;
|
||||
|
||||
|
||||
var relationName;
|
||||
var subInclude = null, scope = null;
|
||||
|
||||
|
||||
if (isPlainObject(include)) {
|
||||
relationName = Object.keys(include)[0];
|
||||
if (include[relationName] instanceof IncludeScope) {
|
||||
|
@ -133,34 +181,540 @@ Inclusion.include = function (objects, include, cb) {
|
|||
subInclude = scope.include();
|
||||
} else {
|
||||
subInclude = include[relationName];
|
||||
//when include = {user:true}, it does not have subInclude
|
||||
if (subInclude === true) {
|
||||
subInclude = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
relationName = include;
|
||||
subInclude = null;
|
||||
}
|
||||
|
||||
|
||||
var relation = relations[relationName];
|
||||
if (!relation) {
|
||||
cb(new Error('Relation "' + relationName + '" is not defined for '
|
||||
+ self.modelName + ' model'));
|
||||
return;
|
||||
}
|
||||
var polymorphic = relation.polymorphic;
|
||||
//if (polymorphic && !polymorphic.discriminator) {
|
||||
// cb(new Error('Relation "' + relationName + '" is polymorphic but ' +
|
||||
// 'discriminator is not present'));
|
||||
// return;
|
||||
//}
|
||||
if (!relation.modelTo) {
|
||||
if (!relation.polymorphic) {
|
||||
cb(new Error('Relation.modelTo is not defined for relation' +
|
||||
relationName + ' and is no polymorphic'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Just skip if inclusion is disabled
|
||||
if (relation.options.disableInclude) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calling the relation method for each object
|
||||
async.each(objs, function (obj, callback) {
|
||||
if(relation.type === 'belongsTo') {
|
||||
//prepare filter and fields for making DB Call
|
||||
var filter = (scope && scope.conditions()) || {};
|
||||
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
|
||||
var includeScope = {};
|
||||
// make sure not to miss any fields for sub includes
|
||||
if (filter.fields && Array.isArray(subInclude) &&
|
||||
relation.modelTo.relations) {
|
||||
includeScope.fields = [];
|
||||
subInclude.forEach(function (name) {
|
||||
var rel = relation.modelTo.relations[name];
|
||||
if (rel && rel.type === 'belongsTo') {
|
||||
includeScope.fields.push(rel.keyFrom);
|
||||
}
|
||||
});
|
||||
}
|
||||
utils.mergeQuery(filter, includeScope, {fields: false});
|
||||
}
|
||||
//Let's add a placeholder where query
|
||||
filter.where = filter.where || {};
|
||||
//if fields are specified, make sure target foreign key is present
|
||||
var fields = filter.fields;
|
||||
if (Array.isArray(fields) && fields.indexOf(relation.keyTo) === -1) {
|
||||
fields.push(relation.keyTo);
|
||||
}
|
||||
else if (isPlainObject(fields) && !fields[relation.keyTo]) {
|
||||
fields[relation.keyTo] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* call relation specific include functions
|
||||
*/
|
||||
if (relation.multiple) {
|
||||
if (relation.modelThrough) {
|
||||
//hasManyThrough needs separate handling
|
||||
return includeHasManyThrough(cb);
|
||||
}
|
||||
//This will also include embedsMany with belongsTo.
|
||||
//Might need to optimize db calls for this.
|
||||
if (relation.type === 'embedsMany') {
|
||||
//embedded docs are part of the objects, no need to make db call.
|
||||
//proceed as implemented earlier.
|
||||
return includeEmbeds(cb);
|
||||
}
|
||||
if (relation.type === 'referencesMany') {
|
||||
return includeReferencesMany(cb);
|
||||
}
|
||||
//assuming all other relations with multiple=true as hasMany
|
||||
return includeHasMany(cb);
|
||||
}
|
||||
else {
|
||||
if (polymorphic) {
|
||||
return includePolymorphic(cb);
|
||||
}
|
||||
if (relation.type === 'embedsOne') {
|
||||
return includeEmbeds(cb);
|
||||
}
|
||||
//hasOne or belongsTo
|
||||
return includeOneToOne(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inclusion of HasManyThrough/HasAndBelongsToMany/Polymorphic
|
||||
* HasManyThrough relations
|
||||
* @param callback
|
||||
*/
|
||||
function includeHasManyThrough(callback) {
|
||||
var sourceIds = [];
|
||||
//Map for Indexing objects by their id for faster retrieval
|
||||
var objIdMap = {};
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
var obj = objs[i];
|
||||
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||
// use modelFrom.keyFrom in where filter later
|
||||
var sourceId = obj[relation.keyFrom];
|
||||
if (sourceId) {
|
||||
sourceIds.push(sourceId);
|
||||
objIdMap[sourceId.toString()] = obj;
|
||||
}
|
||||
// sourceId can be null. but cache empty data as result
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = [];
|
||||
}
|
||||
//default filters are not applicable on through model. should be applied
|
||||
//on modelTo later in 2nd DB call.
|
||||
var throughFilter = {
|
||||
where: {}
|
||||
};
|
||||
throughFilter.where[relation.keyTo] = {
|
||||
inq: sourceIds
|
||||
};
|
||||
if (polymorphic) {
|
||||
//handle polymorphic hasMany (reverse) in which case we need to filter
|
||||
//by discriminator to filter other types
|
||||
throughFilter.where[polymorphic.discriminator] =
|
||||
relation.modelFrom.definition.name;
|
||||
}
|
||||
/**
|
||||
* 1st DB Call of 2 step process. Get through model objects first
|
||||
*/
|
||||
relation.modelThrough.find(throughFilter, throughFetchHandler);
|
||||
/**
|
||||
* Handle the results of Through model objects and fetch the modelTo items
|
||||
* @param err
|
||||
* @param {Array<Model>} throughObjs
|
||||
* @returns {*}
|
||||
*/
|
||||
function throughFetchHandler(err, throughObjs) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// start preparing for 2nd DB call.
|
||||
var targetIds = [];
|
||||
var targetObjsMap = {};
|
||||
for (var i = 0; i < throughObjs.length; i++) {
|
||||
var throughObj = throughObjs[i];
|
||||
var targetId = throughObj[relation.keyThrough];
|
||||
if (targetId) {
|
||||
//save targetIds for 2nd DB Call
|
||||
targetIds.push(targetId);
|
||||
var sourceObj = objIdMap[throughObj[relation.keyTo]];
|
||||
var targetIdStr = targetId.toString();
|
||||
//Since targetId can be duplicates, multiple source objs are put
|
||||
//into buckets.
|
||||
var objList = targetObjsMap[targetIdStr] =
|
||||
targetObjsMap[targetIdStr] || [];
|
||||
objList.push(sourceObj);
|
||||
}
|
||||
}
|
||||
//Polymorphic relation does not have idKey of modelTo. Find it manually
|
||||
var modelToIdName = idName(relation.modelTo);
|
||||
filter.where[modelToIdName] = {
|
||||
inq: targetIds
|
||||
};
|
||||
//make sure that the modelToIdName is included if fields are specified
|
||||
if (Array.isArray(fields) && fields.indexOf(modelToIdName) === -1) {
|
||||
fields.push(modelToIdName);
|
||||
}
|
||||
else if (isPlainObject(fields) && !fields[modelToIdName]) {
|
||||
fields[modelToIdName] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 2nd DB Call of 2 step process. Get modelTo (target) objects
|
||||
*/
|
||||
relation.modelTo.find(filter, targetsFetchHandler);
|
||||
function targetsFetchHandler(err, targets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var tasks = [];
|
||||
//simultaneously process subIncludes. Call it first as it is an async
|
||||
//process.
|
||||
if (subInclude && targets) {
|
||||
tasks.push(function subIncludesTask(next) {
|
||||
relation.modelTo.include(targets, subInclude, next);
|
||||
});
|
||||
}
|
||||
//process & link each target with object
|
||||
tasks.push(targetLinkingTask);
|
||||
function targetLinkingTask(next) {
|
||||
async.each(targets, linkManyToMany, next);
|
||||
function linkManyToMany(target, next) {
|
||||
var targetId = target[modelToIdName];
|
||||
var objList = targetObjsMap[targetId.toString()];
|
||||
async.each(objList, function (obj, next) {
|
||||
obj.__cachedRelations[relationName].push(target);
|
||||
processTargetObj(obj, next);
|
||||
}, next);
|
||||
}
|
||||
}
|
||||
|
||||
execTasksWithInterLeave(tasks, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inclusion of ReferencesMany relation
|
||||
* @param callback
|
||||
*/
|
||||
function includeReferencesMany(callback) {
|
||||
var allTargetIds = [];
|
||||
//Map for Indexing objects by their id for faster retrieval
|
||||
var targetObjsMap = {};
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
var obj = objs[i];
|
||||
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||
// use modelFrom.keyFrom in where filter later
|
||||
var targetIds = obj[relation.keyFrom];
|
||||
if (targetIds) {
|
||||
if (typeof targetIds === 'string') {
|
||||
// For relational DBs, the array is stored as stringified json
|
||||
// Please note obj is a plain object at this point
|
||||
targetIds = JSON.parse(targetIds);
|
||||
}
|
||||
//referencesMany has multiple targetIds per obj. We need to concat
|
||||
// them into allTargetIds before DB Call
|
||||
allTargetIds = allTargetIds.concat(targetIds);
|
||||
for (var j = 0; j < targetIds.length; j++) {
|
||||
var targetId = targetIds[j];
|
||||
var targetIdStr = targetId.toString();
|
||||
var objList = targetObjsMap[targetIdStr] =
|
||||
targetObjsMap[targetIdStr] || [];
|
||||
objList.push(obj);
|
||||
}
|
||||
}
|
||||
// sourceId can be null. but cache empty data as result
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = [];
|
||||
}
|
||||
filter.where[relation.keyTo] = {
|
||||
inq: allTargetIds
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the DB Call, fetch all target objects
|
||||
*/
|
||||
relation.modelTo.find(filter, targetFetchHandler);
|
||||
/**
|
||||
* Handle the fetched target objects
|
||||
* @param err
|
||||
* @param {Array<Model>}targets
|
||||
* @returns {*}
|
||||
*/
|
||||
function targetFetchHandler(err, targets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var tasks = [];
|
||||
//simultaneously process subIncludes
|
||||
if (subInclude && targets) {
|
||||
tasks.push(function subIncludesTask(next) {
|
||||
relation.modelTo.include(targets, subInclude, next);
|
||||
});
|
||||
}
|
||||
//process each target object
|
||||
tasks.push(targetLinkingTask);
|
||||
function targetLinkingTask(next) {
|
||||
async.each(targets, linkManyToMany, next);
|
||||
function linkManyToMany(target, next) {
|
||||
var objList = targetObjsMap[target[relation.keyTo].toString()];
|
||||
async.each(objList, function (obj, next) {
|
||||
obj.__cachedRelations[relationName].push(target);
|
||||
processTargetObj(obj, next);
|
||||
}, next);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
execTasksWithInterLeave(tasks, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inclusion of HasMany relation
|
||||
* @param callback
|
||||
*/
|
||||
function includeHasMany(callback) {
|
||||
var sourceIds = [];
|
||||
//Map for Indexing objects by their id for faster retrieval
|
||||
var objIdMap = {};
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
var obj = objs[i];
|
||||
// one-to-many: foreign key reference is modelTo -> modelFrom.
|
||||
// use modelFrom.keyFrom in where filter later
|
||||
var sourceId = obj[relation.keyFrom];
|
||||
if (sourceId) {
|
||||
sourceIds.push(sourceId);
|
||||
objIdMap[sourceId.toString()] = obj;
|
||||
}
|
||||
// sourceId can be null. but cache empty data as result
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = [];
|
||||
}
|
||||
filter.where[relation.keyTo] = {
|
||||
inq: sourceIds
|
||||
};
|
||||
relation.modelTo.find(filter, targetFetchHandler);
|
||||
/**
|
||||
* Process fetched related objects
|
||||
* @param err
|
||||
* @param {Array<Model>} targets
|
||||
* @returns {*}
|
||||
*/
|
||||
function targetFetchHandler(err, targets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var tasks = [];
|
||||
//simultaneously process subIncludes
|
||||
if (subInclude && targets) {
|
||||
tasks.push(function subIncludesTask(next) {
|
||||
relation.modelTo.include(targets, subInclude, next);
|
||||
});
|
||||
}
|
||||
//process each target object
|
||||
tasks.push(targetLinkingTask);
|
||||
function targetLinkingTask(next) {
|
||||
async.each(targets, linkManyToOne, next);
|
||||
function linkManyToOne(target, next) {
|
||||
var obj = objIdMap[target[relation.keyTo].toString()];
|
||||
obj.__cachedRelations[relationName].push(target);
|
||||
processTargetObj(obj, next);
|
||||
}
|
||||
}
|
||||
|
||||
execTasksWithInterLeave(tasks, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Inclusion of Polymorphic BelongsTo/HasOne relation
|
||||
* @param callback
|
||||
*/
|
||||
function includePolymorphic(callback) {
|
||||
var targetIdsByType = {};
|
||||
//Map for Indexing objects by their type and targetId for faster retrieval
|
||||
var targetObjMapByType = {};
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
var obj = objs[i];
|
||||
var discriminator = polymorphic.discriminator;
|
||||
var modelType = obj[discriminator];
|
||||
if (modelType) {
|
||||
targetIdsByType[modelType] = targetIdsByType[modelType] || [];
|
||||
targetObjMapByType[modelType] = targetObjMapByType[modelType] || {};
|
||||
var targetIds = targetIdsByType[modelType];
|
||||
var targetObjsMap = targetObjMapByType[modelType];
|
||||
var targetId = obj[relation.keyFrom];
|
||||
if (targetId) {
|
||||
targetIds.push(targetId);
|
||||
var targetIdStr = targetId.toString();
|
||||
targetObjsMap[targetIdStr] = targetObjsMap[targetIdStr] || [];
|
||||
//Is belongsTo. Multiple objects can have the same
|
||||
//targetId and therefore map value is an array
|
||||
targetObjsMap[targetIdStr].push(obj);
|
||||
}
|
||||
}
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = null;
|
||||
}
|
||||
async.each(Object.keys(targetIdsByType), processPolymorphicType,
|
||||
callback);
|
||||
/**
|
||||
* Process Polymorphic objects of each type (modelType)
|
||||
* @param {String} modelType
|
||||
* @param callback
|
||||
*/
|
||||
function processPolymorphicType(modelType, callback) {
|
||||
var typeFilter = {where: {}};
|
||||
utils.mergeQuery(typeFilter, filter);
|
||||
var targetIds = targetIdsByType[modelType];
|
||||
typeFilter.where[relation.keyTo] = {
|
||||
inq: targetIds
|
||||
};
|
||||
var app = relation.modelFrom.app;
|
||||
var Model = lookupModel(relation.modelFrom.dataSource.modelBuilder.
|
||||
models, modelType);
|
||||
if (!Model) {
|
||||
callback(new Error('Discriminator type "' + modelType +
|
||||
' specified but no model exists with such name'));
|
||||
return;
|
||||
}
|
||||
Model.find(typeFilter, targetFetchHandler);
|
||||
/**
|
||||
* Process fetched related objects
|
||||
* @param err
|
||||
* @param {Array<Model>} targets
|
||||
* @returns {*}
|
||||
*/
|
||||
function targetFetchHandler(err, targets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var tasks = [];
|
||||
|
||||
//simultaneously process subIncludes
|
||||
if (subInclude && targets) {
|
||||
tasks.push(function subIncludesTask(next) {
|
||||
Model.include(targets, subInclude, next);
|
||||
});
|
||||
}
|
||||
//process each target object
|
||||
tasks.push(targetLinkingTask);
|
||||
function targetLinkingTask(next) {
|
||||
var targetObjsMap = targetObjMapByType[modelType];
|
||||
async.each(targets, linkOneToMany, next);
|
||||
function linkOneToMany(target, next) {
|
||||
var objList = targetObjsMap[target[relation.keyTo].toString()];
|
||||
async.each(objList, function (obj, next) {
|
||||
obj.__cachedRelations[relationName] = target;
|
||||
processTargetObj(obj, next);
|
||||
}, next);
|
||||
}
|
||||
}
|
||||
|
||||
execTasksWithInterLeave(tasks, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Inclusion of BelongsTo/HasOne relation
|
||||
* @param callback
|
||||
*/
|
||||
function includeOneToOne(callback) {
|
||||
var targetIds = [];
|
||||
var objTargetIdMap = {};
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
var obj = objs[i];
|
||||
if (relation.type === 'belongsTo') {
|
||||
if (obj[relation.keyFrom] === null ||
|
||||
obj[relation.keyFrom] === undefined) {
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var targetId = obj[relation.keyFrom];
|
||||
if (targetId) {
|
||||
targetIds.push(targetId);
|
||||
var targetIdStr = targetId.toString();
|
||||
objTargetIdMap[targetIdStr] = objTargetIdMap[targetIdStr] || [];
|
||||
objTargetIdMap[targetIdStr].push(obj);
|
||||
}
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = null;
|
||||
}
|
||||
filter.where[relation.keyTo] = {
|
||||
inq: targetIds
|
||||
};
|
||||
relation.modelTo.find(filter, targetFetchHandler);
|
||||
/**
|
||||
* Process fetched related objects
|
||||
* @param err
|
||||
* @param {Array<Model>} targets
|
||||
* @returns {*}
|
||||
*/
|
||||
function targetFetchHandler(err, targets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var tasks = [];
|
||||
//simultaneously process subIncludes
|
||||
if (subInclude && targets) {
|
||||
tasks.push(function subIncludesTask(next) {
|
||||
relation.modelTo.include(targets, subInclude, next);
|
||||
});
|
||||
}
|
||||
//process each target object
|
||||
tasks.push(targetLinkingTask);
|
||||
function targetLinkingTask(next) {
|
||||
async.each(targets, linkOneToMany, next);
|
||||
function linkOneToMany(target, next) {
|
||||
var targetId = target[relation.keyTo];
|
||||
var objList = objTargetIdMap[targetId.toString()];
|
||||
async.each(objList, function (obj, next) {
|
||||
obj.__cachedRelations[relationName] = target;
|
||||
processTargetObj(obj, next);
|
||||
}, next);
|
||||
}
|
||||
}
|
||||
|
||||
execTasksWithInterLeave(tasks, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle Inclusion of EmbedsMany/EmbedsManyWithBelongsTo/EmbedsOne
|
||||
* Relations. Since Embedded docs are part of parents, no need to make
|
||||
* db calls. Let the related function be called for each object to fetch
|
||||
* the results from cache.
|
||||
*
|
||||
* TODO: Optimize EmbedsManyWithBelongsTo relation DB Calls
|
||||
* @param callback
|
||||
*/
|
||||
function includeEmbeds(callback) {
|
||||
async.each(objs, function (obj, next) {
|
||||
processTargetObj(obj, next);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Each Model Object and make sure specified relations are included
|
||||
* @param {Model} obj - Single Mode object for which inclusion is needed
|
||||
* @param callback
|
||||
* @returns {*}
|
||||
*/
|
||||
function processTargetObj(obj, callback) {
|
||||
var inst = (obj instanceof self) ? obj : new self(obj);
|
||||
// Calling the relation method on the instance
|
||||
if (relation.type === 'belongsTo') {
|
||||
// If the belongsTo relation doesn't have an owner
|
||||
if(obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
|
||||
if (obj[relation.keyFrom] === null || obj[relation.keyFrom] === undefined) {
|
||||
defineCachedRelations(obj);
|
||||
// Set to null if the owner doesn't exist
|
||||
obj.__cachedRelations[relationName] = null;
|
||||
if(obj === inst) {
|
||||
if (obj === inst) {
|
||||
obj.__data[relationName] = null;
|
||||
} else {
|
||||
obj[relationName] = null;
|
||||
|
@ -168,16 +722,35 @@ Inclusion.include = function (objects, include, cb) {
|
|||
return callback();
|
||||
}
|
||||
}
|
||||
|
||||
var inst = (obj instanceof self) ? obj : new self(obj);
|
||||
// Calling the relation method on the instance
|
||||
|
||||
/**
|
||||
* Sets the related objects as a property of Parent Object
|
||||
* @param {Array<Model>|Model|null} result - Related Object/Objects
|
||||
* @param cb
|
||||
*/
|
||||
function setIncludeData(result, cb) {
|
||||
if (obj === inst) {
|
||||
obj.__data[relationName] = result;
|
||||
obj.setStrict(false);
|
||||
} else {
|
||||
obj[relationName] = result;
|
||||
}
|
||||
cb(null, result);
|
||||
}
|
||||
|
||||
//obj.__cachedRelations[relationName] can be null if no data was returned
|
||||
if (obj.__cachedRelations &&
|
||||
obj.__cachedRelations[relationName] !== undefined) {
|
||||
return setIncludeData(obj.__cachedRelations[relationName],
|
||||
callback);
|
||||
}
|
||||
//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
|
||||
|
||||
|
||||
if ((relation.multiple || relation.type === 'belongsTo') && scope) {
|
||||
var includeScope = {};
|
||||
var filter = scope.conditions();
|
||||
|
||||
|
||||
// make sure not to miss any fields for sub includes
|
||||
if (filter.fields && Array.isArray(subInclude) && relation.modelTo.relations) {
|
||||
includeScope.fields = [];
|
||||
|
@ -188,39 +761,26 @@ Inclusion.include = function (objects, include, cb) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
utils.mergeQuery(filter, includeScope, {fields: false});
|
||||
|
||||
|
||||
related = inst[relationName].bind(inst, filter);
|
||||
} else {
|
||||
related = inst[relationName].bind(inst);
|
||||
}
|
||||
|
||||
|
||||
related(function (err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
} else {
|
||||
|
||||
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = result;
|
||||
|
||||
if(obj === inst) {
|
||||
obj.__data[relationName] = result;
|
||||
obj.setStrict(false);
|
||||
} else {
|
||||
obj[relationName] = result;
|
||||
}
|
||||
|
||||
if (subInclude && result) {
|
||||
var subItems = relation.multiple ? result : [result];
|
||||
// Recursively include the related models
|
||||
relation.modelTo.include(subItems, subInclude, callback);
|
||||
} else {
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
return setIncludeData(result, callback);
|
||||
}
|
||||
});
|
||||
}, cb);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "loopback-datasource-juggler",
|
||||
"version": "2.26.4",
|
||||
"version": "2.27.0",
|
||||
"description": "LoopBack DataSoure Juggler",
|
||||
"keywords": [
|
||||
"StrongLoop",
|
||||
|
@ -28,7 +28,8 @@
|
|||
"devDependencies": {
|
||||
"bluebird": "^2.9.9",
|
||||
"mocha": "^2.1.0",
|
||||
"should": "^5.0.0"
|
||||
"should": "^5.0.0",
|
||||
"sinon": "^1.14.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^0.9.0",
|
||||
|
|
|
@ -136,11 +136,17 @@ describe('datatypes', function () {
|
|||
function testDataInDB(done) {
|
||||
|
||||
// verify that the value stored in the db is still an object
|
||||
db.connector.find(Model.modelName, id, function (err, data) {
|
||||
function cb(err, data) {
|
||||
should.exist(data);
|
||||
data.num.should.be.type('number');
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
if (db.connector.find.length === 4) {
|
||||
db.connector.find(Model.modelName, id, {}, cb);
|
||||
} else {
|
||||
db.connector.find(Model.modelName, id, cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -252,16 +258,27 @@ describe('datatypes', function () {
|
|||
created.should.have.properties(EXPECTED);
|
||||
saved.should.have.properties(EXPECTED);
|
||||
|
||||
TestModel.dataSource.connector.all(
|
||||
TestModel.modelName,
|
||||
{ where: { id: created.id } },
|
||||
function(err, found) {
|
||||
if (err) return done(err);
|
||||
should.exist(found[0]);
|
||||
found[0].should.have.properties(EXPECTED);
|
||||
done();
|
||||
}
|
||||
);
|
||||
function cb(err, found) {
|
||||
if (err) return done(err);
|
||||
should.exist(found[0]);
|
||||
found[0].should.have.properties(EXPECTED);
|
||||
done();
|
||||
}
|
||||
|
||||
if (TestModel.dataSource.connector.all.length === 4) {
|
||||
TestModel.dataSource.connector.all(
|
||||
TestModel.modelName,
|
||||
{where: {id: created.id}},
|
||||
{},
|
||||
cb
|
||||
);
|
||||
} else {
|
||||
TestModel.dataSource.connector.all(
|
||||
TestModel.modelName,
|
||||
{where: {id: created.id}},
|
||||
cb
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// This test written in mocha+should.js
|
||||
var should = require('./init.js');
|
||||
var sinon = require('sinon');
|
||||
var async = require('async');
|
||||
|
||||
var db, User, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
|
||||
var db, User, Profile, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
|
||||
|
||||
describe('include', function () {
|
||||
|
||||
|
@ -334,20 +336,203 @@ describe('include', function () {
|
|||
});
|
||||
});
|
||||
|
||||
// Not implemented correctly, see: loopback-datasource-juggler/issues/166
|
||||
//
|
||||
// it('should support include scope on hasAndBelongsToMany', function (done) {
|
||||
// Assembly.find({include: { relation: 'parts', scope: {
|
||||
// where: { partNumber: 'engine' }
|
||||
// }}}, function (err, assemblies) {
|
||||
// assemblies.length.should.equal(1);
|
||||
// var parts = assemblies[0].parts();
|
||||
// parts.should.have.length(1);
|
||||
// parts[0].partNumber.should.equal('engine');
|
||||
// done();
|
||||
// });
|
||||
// });
|
||||
it('should fetch User - Profile (HasOne)', function (done) {
|
||||
User.find({include: ['profile']}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
should.exist(users);
|
||||
users.length.should.be.ok;
|
||||
var usersWithProfile = 0;
|
||||
users.forEach(function (user) {
|
||||
// The relation should be promoted as the 'owner' property
|
||||
user.should.have.property('profile');
|
||||
var userObj = user.toJSON();
|
||||
var profile = user.profile();
|
||||
if (profile) {
|
||||
profile.should.be.an.instanceOf(Profile);
|
||||
usersWithProfile++;
|
||||
}
|
||||
else {
|
||||
(profile === null).should.be.true;
|
||||
}
|
||||
// The __cachedRelations should be removed from json output
|
||||
userObj.should.not.have.property('__cachedRelations');
|
||||
user.__cachedRelations.should.have.property('profile');
|
||||
if (user.__cachedRelations.profile) {
|
||||
user.__cachedRelations.profile.userId.should.eql(user.id);
|
||||
usersWithProfile++;
|
||||
}
|
||||
});
|
||||
usersWithProfile.should.equal(2 * 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Not implemented correctly, see: loopback-datasource-juggler/issues/166
|
||||
// fixed by DB optimization
|
||||
it('should support include scope on hasAndBelongsToMany', function (done) {
|
||||
Assembly.find({include: { relation: 'parts', scope: {
|
||||
where: { partNumber: 'engine' }
|
||||
}}}, function (err, assemblies) {
|
||||
assemblies.length.should.equal(1);
|
||||
var parts = assemblies[0].parts();
|
||||
parts.should.have.length(1);
|
||||
parts[0].partNumber.should.equal('engine');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe(' performance - ', function () {
|
||||
beforeEach(function () {
|
||||
this.callSpy = sinon.spy(db.connector, 'all');
|
||||
});
|
||||
afterEach(function () {
|
||||
db.connector.all.restore();
|
||||
});
|
||||
it('including belongsTo should make only 2 db calls', function (done) {
|
||||
var self = this;
|
||||
Passport.find({include: 'owner'}, function (err, passports) {
|
||||
passports.length.should.be.ok;
|
||||
passports.forEach(function (p) {
|
||||
p.__cachedRelations.should.have.property('owner');
|
||||
// The relation should be promoted as the 'owner' property
|
||||
p.should.have.property('owner');
|
||||
// The __cachedRelations should be removed from json output
|
||||
p.toJSON().should.not.have.property('__cachedRelations');
|
||||
var owner = p.__cachedRelations.owner;
|
||||
if (!p.ownerId) {
|
||||
should.not.exist(owner);
|
||||
} else {
|
||||
should.exist(owner);
|
||||
owner.id.should.eql(p.ownerId);
|
||||
}
|
||||
});
|
||||
self.callSpy.calledTwice.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('including hasManyThrough should make only 3 db calls', function (done) {
|
||||
var self = this;
|
||||
Assembly.create([{name: 'sedan'}, {name: 'hatchback'},
|
||||
{name: 'SUV'}],
|
||||
function (err, assemblies) {
|
||||
Part.create([{partNumber: 'engine'}, {partNumber: 'bootspace'},
|
||||
{partNumber: 'silencer'}],
|
||||
function (err, parts) {
|
||||
async.each(parts, function (part, next) {
|
||||
async.each(assemblies, function (assembly, next) {
|
||||
if (assembly.name === 'SUV') {
|
||||
return next();
|
||||
}
|
||||
if (assembly.name === 'hatchback' &&
|
||||
part.partNumber === 'bootspace') {
|
||||
return next();
|
||||
}
|
||||
assembly.parts.add(part, function (err, data) {
|
||||
next();
|
||||
});
|
||||
}, next);
|
||||
}, function (err) {
|
||||
self.callSpy.reset();
|
||||
Assembly.find({
|
||||
where: {
|
||||
name: {
|
||||
inq: ['sedan', 'hatchback', 'SUV']
|
||||
}
|
||||
},
|
||||
include: 'parts'
|
||||
}, function (err, result) {
|
||||
should.not.exist(err);
|
||||
should.exists(result);
|
||||
result.length.should.equal(3);
|
||||
//sedan
|
||||
result[0].parts().should.have.length(3);
|
||||
//hatcback
|
||||
result[1].parts().should.have.length(2);
|
||||
//SUV
|
||||
result[2].parts().should.have.length(0);
|
||||
self.callSpy.calledThrice.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('including hasMany should make only 2 db calls', function (done) {
|
||||
var self = this;
|
||||
User.find({include: ['posts', 'passports']}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
should.exist(users);
|
||||
users.length.should.be.ok;
|
||||
users.forEach(function (user) {
|
||||
// The relation should be promoted as the 'owner' property
|
||||
user.should.have.property('posts');
|
||||
user.should.have.property('passports');
|
||||
|
||||
var userObj = user.toJSON();
|
||||
userObj.should.have.property('posts');
|
||||
userObj.should.have.property('passports');
|
||||
userObj.posts.should.be.an.instanceOf(Array);
|
||||
userObj.passports.should.be.an.instanceOf(Array);
|
||||
|
||||
// The __cachedRelations should be removed from json output
|
||||
userObj.should.not.have.property('__cachedRelations');
|
||||
|
||||
user.__cachedRelations.should.have.property('posts');
|
||||
user.__cachedRelations.should.have.property('passports');
|
||||
user.__cachedRelations.posts.forEach(function (p) {
|
||||
p.userId.should.eql(user.id);
|
||||
});
|
||||
user.__cachedRelations.passports.forEach(function (pp) {
|
||||
pp.ownerId.should.eql(user.id);
|
||||
});
|
||||
});
|
||||
self.callSpy.calledThrice.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not make n+1 db calls in relation syntax',
|
||||
function (done) {
|
||||
var self = this;
|
||||
User.find({include: [{ relation: 'posts', scope: {
|
||||
where: {title: 'Post A'}
|
||||
}}, 'passports']}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
should.exist(users);
|
||||
users.length.should.be.ok;
|
||||
users.forEach(function (user) {
|
||||
// The relation should be promoted as the 'owner' property
|
||||
user.should.have.property('posts');
|
||||
user.should.have.property('passports');
|
||||
|
||||
var userObj = user.toJSON();
|
||||
userObj.should.have.property('posts');
|
||||
userObj.should.have.property('passports');
|
||||
userObj.posts.should.be.an.instanceOf(Array);
|
||||
userObj.passports.should.be.an.instanceOf(Array);
|
||||
|
||||
// The __cachedRelations should be removed from json output
|
||||
userObj.should.not.have.property('__cachedRelations');
|
||||
|
||||
user.__cachedRelations.should.have.property('posts');
|
||||
user.__cachedRelations.should.have.property('passports');
|
||||
user.__cachedRelations.posts.forEach(function (p) {
|
||||
p.userId.should.eql(user.id);
|
||||
p.title.should.be.equal('Post A');
|
||||
});
|
||||
user.__cachedRelations.passports.forEach(function (pp) {
|
||||
pp.ownerId.should.eql(user.id);
|
||||
});
|
||||
});
|
||||
self.callSpy.calledThrice.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setup(done) {
|
||||
|
@ -359,6 +544,9 @@ function setup(done) {
|
|||
name: String,
|
||||
age: Number
|
||||
});
|
||||
Profile = db.define('Profile', {
|
||||
profileName: String
|
||||
});
|
||||
AccessToken = db.define('AccessToken', {
|
||||
token: String
|
||||
});
|
||||
|
@ -376,6 +564,8 @@ function setup(done) {
|
|||
foreignKey: 'userId',
|
||||
options: {disableInclude: true}
|
||||
});
|
||||
Profile.belongsTo('user', {model: User});
|
||||
User.hasOne('profile', {foreignKey: 'userId'});
|
||||
Post.belongsTo('author', {model: User, foreignKey: 'userId'});
|
||||
|
||||
Assembly = db.define('Assembly', {
|
||||
|
@ -392,6 +582,7 @@ function setup(done) {
|
|||
db.automigrate(function () {
|
||||
var createdUsers = [];
|
||||
var createdPassports = [];
|
||||
var createdProfiles = [];
|
||||
var createdPosts = [];
|
||||
createUsers();
|
||||
function createUsers() {
|
||||
|
@ -438,6 +629,21 @@ function setup(done) {
|
|||
);
|
||||
}
|
||||
|
||||
function createProfiles() {
|
||||
clearAndCreate(
|
||||
Profile,
|
||||
[
|
||||
{profileName: 'Profile A', userId: createdUsers[0].id},
|
||||
{profileName: 'Profile B', userId: createdUsers[1].id},
|
||||
{profileName: 'Profile Z'}
|
||||
],
|
||||
function (items) {
|
||||
createdProfiles = items
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createPosts() {
|
||||
clearAndCreate(
|
||||
Post,
|
||||
|
@ -450,7 +656,7 @@ function setup(done) {
|
|||
],
|
||||
function (items) {
|
||||
createdPosts = items;
|
||||
done();
|
||||
createProfiles();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -238,7 +238,7 @@ describe('Memory connector', function() {
|
|||
});
|
||||
|
||||
it('should successfully extract 1 user (Lennon) from the db', function(done) {
|
||||
User.find({where: {birthday: {between: [new Date(1970,0),new Date(1990,0)]}}},
|
||||
User.find({where: {birthday: {between: [new Date(1970,0),new Date(1990,0)]}}},
|
||||
function(err, users) {
|
||||
should(users.length).be.equal(1);
|
||||
should(users[0].name).be.equal('John Lennon');
|
||||
|
@ -247,7 +247,7 @@ describe('Memory connector', function() {
|
|||
});
|
||||
|
||||
it('should successfully extract 2 users from the db', function(done) {
|
||||
User.find({where: {birthday: {between: [new Date(1940,0),new Date(1990,0)]}}},
|
||||
User.find({where: {birthday: {between: [new Date(1940,0),new Date(1990,0)]}}},
|
||||
function(err, users) {
|
||||
should(users.length).be.equal(2);
|
||||
done();
|
||||
|
@ -255,7 +255,7 @@ describe('Memory connector', function() {
|
|||
});
|
||||
|
||||
it('should successfully extract 0 user from the db', function(done) {
|
||||
User.find({where: {birthday: {between: [new Date(1990,0), Date.now()]}}},
|
||||
User.find({where: {birthday: {between: [new Date(1990,0), Date.now()]}}},
|
||||
function(err, users) {
|
||||
should(users.length).be.equal(0);
|
||||
done();
|
||||
|
@ -489,9 +489,9 @@ describe('Optimized connector', function() {
|
|||
|
||||
// optimized methods
|
||||
ds.connector.findOrCreate = function (model, query, data, callback) {
|
||||
this.all(model, query, function (err, list) {
|
||||
this.all(model, query, {}, function (err, list) {
|
||||
if (err || (list && list[0])) return callback(err, list && list[0], false);
|
||||
this.create(model, data, function (err) {
|
||||
this.create(model, data, {}, function (err) {
|
||||
callback(err, data, true);
|
||||
});
|
||||
}.bind(this));
|
||||
|
@ -509,5 +509,79 @@ describe('Unoptimized connector', function() {
|
|||
require('./persistence-hooks.suite')(ds, should);
|
||||
});
|
||||
|
||||
describe('Memory connector with options', function() {
|
||||
var ds, savedOptions = {}, Post;
|
||||
|
||||
before(function() {
|
||||
ds = new DataSource({connector: 'memory'});
|
||||
ds.connector.create = function(model, data, options, cb) {
|
||||
savedOptions.create = options;
|
||||
process.nextTick(function() {
|
||||
cb(null, 1);
|
||||
});
|
||||
};
|
||||
|
||||
ds.connector.update = function(model, where, data, options, cb) {
|
||||
savedOptions.update = options;
|
||||
process.nextTick(function() {
|
||||
cb(null, {count: 1});
|
||||
});
|
||||
};
|
||||
|
||||
ds.connector.all = function(model, filter, options, cb) {
|
||||
savedOptions.find = options;
|
||||
process.nextTick(function() {
|
||||
cb(null, [{title: 't1', content: 'c1'}]);
|
||||
});
|
||||
};
|
||||
|
||||
Post = ds.define('Post', {
|
||||
title: String,
|
||||
content: String
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive options from the find method', function(done) {
|
||||
var opts = {transaction: 'tx1'};
|
||||
Post.find({where: {title: 't1'}}, opts, function(err, p) {
|
||||
savedOptions.find.should.be.eql(opts);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive options from the find method', function(done) {
|
||||
var opts = {transaction: 'tx2'};
|
||||
Post.find({}, opts, function(err, p) {
|
||||
savedOptions.find.should.be.eql(opts);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should treat first object arg as filter for find', function(done) {
|
||||
var filter = {title: 't1'};
|
||||
Post.find(filter, function(err, p) {
|
||||
savedOptions.find.should.be.eql({});
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive options from the create method', function(done) {
|
||||
var opts = {transaction: 'tx3'};
|
||||
Post.create({title: 't1', content: 'c1'}, opts, function(err, p) {
|
||||
savedOptions.create.should.be.eql(opts);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive options from the update method', function(done) {
|
||||
var opts = {transaction: 'tx4'};
|
||||
Post.update({title: 't1'}, {content: 'c1 --> c2'},
|
||||
opts, function(err, p) {
|
||||
savedOptions.update.should.be.eql(opts);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue