Merge branch 'release/2.10.0' into production

This commit is contained in:
Raymond Feng 2014-10-13 16:10:17 -07:00
commit 9807f2dbb7
19 changed files with 1692 additions and 297 deletions

151
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,151 @@
### Contributing ###
Thank you for your interest in `loopback-datasource-juggler`, an open source project
administered by StrongLoop.
Contributing to `loopback-datasource-juggler` is easy. In a few simple steps:
* Ensure that your effort is aligned with the project's roadmap by
talking to the maintainers, especially if you are going to spend a
lot of time on it.
* Make something better or fix a bug.
* Adhere to code style outlined in the [Google C++ Style Guide][] and
[Google Javascript Style Guide][].
* Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback-datasource-juggler)
* Submit a pull request through Github.
### Contributor License Agreement ###
```
Individual Contributor License Agreement
By signing this Individual Contributor License Agreement
("Agreement"), and making a Contribution (as defined below) to
StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and
agree to the following terms and conditions for Your present and
future Contributions submitted to StrongLoop. Except for the license
granted in this Agreement to StrongLoop and recipients of software
distributed by StrongLoop, You reserve all right, title, and interest
in and to Your Contributions.
1. Definitions
"You" or "Your" shall mean the copyright owner or the individual
authorized by the copyright owner that is entering into this
Agreement with StrongLoop.
"Contribution" shall mean any original work of authorship,
including any modifications or additions to an existing work, that
is intentionally submitted by You to StrongLoop for inclusion in,
or documentation of, any of the products owned or managed by
StrongLoop ("Work"). For purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication
sent to StrongLoop or its representatives, including but not
limited to communication or electronic mailing lists, source code
control systems, and issue tracking systems that are managed by,
or on behalf of, StrongLoop for the purpose of discussing and
improving the Work, but excluding communication that is
conspicuously marked or otherwise designated in writing by You as
"Not a Contribution."
2. You Grant a Copyright License to StrongLoop
Subject to the terms and conditions of this Agreement, You hereby
grant to StrongLoop and recipients of software distributed by
StrongLoop, a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable copyright license to reproduce, prepare
derivative works of, publicly display, publicly perform,
sublicense, and distribute Your Contributions and such derivative
works under any license and without any restrictions.
3. You Grant a Patent License to StrongLoop
Subject to the terms and conditions of this Agreement, You hereby
grant to StrongLoop and to recipients of software distributed by
StrongLoop a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this Section)
patent license to make, have made, use, offer to sell, sell,
import, and otherwise transfer the Work under any license and
without any restrictions. The patent license You grant to
StrongLoop under this Section applies only to those patent claims
licensable by You that are necessarily infringed by Your
Contributions(s) alone or by combination of Your Contributions(s)
with the Work to which such Contribution(s) was submitted. If any
entity institutes a patent litigation against You or any other
entity (including a cross-claim or counterclaim in a lawsuit)
alleging that Your Contribution, or the Work to which You have
contributed, constitutes direct or contributory patent
infringement, any patent licenses granted to that entity under
this Agreement for that Contribution or Work shall terminate as
of the date such litigation is filed.
4. You Have the Right to Grant Licenses to StrongLoop
You represent that You are legally entitled to grant the licenses
in this Agreement.
If Your employer(s) has rights to intellectual property that You
create, You represent that You have received permission to make
the Contributions on behalf of that employer, that Your employer
has waived such rights for Your Contributions, or that Your
employer has executed a separate Corporate Contributor License
Agreement with StrongLoop.
5. The Contributions Are Your Original Work
You represent that each of Your Contributions are Your original
works of authorship (see Section 8 (Submissions on Behalf of
Others) for submission on behalf of others). You represent that to
Your knowledge, no other person claims, or has the right to claim,
any right in any intellectual property right related to Your
Contributions.
You also represent that You are not legally obligated, whether by
entering into an agreement or otherwise, in any way that conflicts
with the terms of this Agreement.
You represent that Your Contribution submissions include complete
details of any third-party license or other restriction (including,
but not limited to, related patents and trademarks) of which You
are personally aware and which are associated with any part of
Your Contributions.
6. You Don't Have an Obligation to Provide Support for Your Contributions
You are not expected to provide support for Your Contributions,
except to the extent You desire to provide support. You may provide
support for free, for a fee, or not at all.
6. No Warranties or Conditions
StrongLoop acknowledges that unless required by applicable law or
agreed to in writing, You provide Your Contributions on an "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR
FITNESS FOR A PARTICULAR PURPOSE.
7. Submission on Behalf of Others
If You wish to submit work that is not Your original creation, You
may submit it to StrongLoop separately from any Contribution,
identifying the complete details of its source and of any license
or other restriction (including, but not limited to, related
patents, trademarks, and license agreements) of which You are
personally aware, and conspicuously marking the work as
"Submitted on Behalf of a Third-Party: [named here]".
8. Agree to Notify of Change of Circumstances
You agree to notify StrongLoop of any facts or circumstances of
which You become aware that would make these representations
inaccurate in any respect. Email us at callback@strongloop.com.
```
[Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml
[Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml

View File

@ -70,6 +70,31 @@ function deserialize(dbObj) {
} }
} }
Memory.prototype.getCollection = function(model) {
var modelClass = this._models[model];
if (modelClass.settings.memory) {
model = modelClass.settings.memory.collection || model;
}
return model;
}
Memory.prototype.initCollection = function(model) {
this.collection(model, {});
this.collectionSeq(model, 1);
}
Memory.prototype.collection = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.cache[model] = val;
return this.cache[model];
};
Memory.prototype.collectionSeq = function(model, val) {
model = this.getCollection(model);
if (arguments.length > 1) this.ids[model] = val;
return this.ids[model];
};
Memory.prototype.loadFromFile = function(callback) { Memory.prototype.loadFromFile = function(callback) {
var self = this; var self = this;
var hasLocalStorage = typeof window !== 'undefined' && window.localStorage; var hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
@ -161,36 +186,31 @@ Memory.prototype.saveToFile = function (result, callback) {
Memory.prototype.define = function defineModel(definition) { Memory.prototype.define = function defineModel(definition) {
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments)); this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
var m = definition.model.modelName; var m = definition.model.modelName;
if(!this.cache[m]) { if(!this.collection(m)) this.initCollection(m);
this.cache[m] = {};
this.ids[m] = 1;
}
}; };
Memory.prototype.create = function create(model, data, callback) { Memory.prototype.create = function create(model, data, callback) {
// FIXME: [rfeng] We need to generate unique ids based on the id type // FIXME: [rfeng] We need to generate unique ids based on the id type
// FIXME: [rfeng] We don't support composite ids yet // FIXME: [rfeng] We don't support composite ids yet
var currentId = this.ids[model]; var currentId = this.collectionSeq(model);
if (currentId === undefined) { if (currentId === undefined) { // First time
// First time currentId = this.collectionSeq(model, 1);
this.ids[model] = 1;
currentId = 1;
} }
var id = this.getIdValue(model, data) || currentId; var id = this.getIdValue(model, data) || currentId;
if (id > currentId) { if (id > currentId) {
// If the id is passed in and the value is greater than the current id // If the id is passed in and the value is greater than the current id
currentId = id; currentId = id;
} }
this.ids[model] = Number(currentId) + 1; this.collectionSeq(model, Number(currentId) + 1);
var props = this._models[model].properties; var props = this._models[model].properties;
var idName = this.idName(model); var idName = this.idName(model);
id = (props[idName] && props[idName].type && props[idName].type(id)) || id; id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id); this.setIdValue(model, data, id);
if(!this.cache[model]) { if(!this.collection(model)) {
this.cache[model] = {}; this.collection(model, {});
} }
this.cache[model][id] = serialize(data); this.collection(model)[id] = serialize(data);
this.saveToFile(id, callback); this.saveToFile(id, callback);
}; };
@ -210,30 +230,30 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
Memory.prototype.save = function save(model, data, callback) { Memory.prototype.save = function save(model, data, callback) {
var id = this.getIdValue(model, data); var id = this.getIdValue(model, data);
var cachedModels = this.cache[model]; var cachedModels = this.collection(model);
var modelData = cachedModels && this.cache[model][id]; var modelData = cachedModels && this.collection(model)[id];
modelData = modelData && deserialize(modelData); modelData = modelData && deserialize(modelData);
if (modelData) { if (modelData) {
data = merge(modelData, data); data = merge(modelData, data);
} }
this.cache[model][id] = serialize(data); this.collection(model)[id] = serialize(data);
this.saveToFile(data, callback); this.saveToFile(data, callback);
}; };
Memory.prototype.exists = function exists(model, id, callback) { Memory.prototype.exists = function exists(model, id, callback) {
process.nextTick(function () { process.nextTick(function () {
callback(null, this.cache[model] && this.cache[model].hasOwnProperty(id)); callback(null, this.collection(model) && this.collection(model).hasOwnProperty(id));
}.bind(this)); }.bind(this));
}; };
Memory.prototype.find = function find(model, id, callback) { Memory.prototype.find = function find(model, id, callback) {
process.nextTick(function () { process.nextTick(function () {
callback(null, id in this.cache[model] && this.fromDb(model, this.cache[model][id])); callback(null, id in this.collection(model) && this.fromDb(model, this.collection(model)[id]));
}.bind(this)); }.bind(this));
}; };
Memory.prototype.destroy = function destroy(model, id, callback) { Memory.prototype.destroy = function destroy(model, id, callback) {
delete this.cache[model][id]; delete this.collection(model)[id];
this.saveToFile(null, callback); this.saveToFile(null, callback);
}; };
@ -266,8 +286,8 @@ Memory.prototype.fromDb = function (model, data) {
Memory.prototype.all = function all(model, filter, callback) { Memory.prototype.all = function all(model, filter, callback) {
var self = this; var self = this;
var nodes = Object.keys(this.cache[model]).map(function (key) { var nodes = Object.keys(this.collection(model)).map(function (key) {
return this.fromDb(model, this.cache[model][key]); return this.fromDb(model, this.collection(model)[key]);
}.bind(this)); }.bind(this));
if (filter) { if (filter) {
@ -505,24 +525,23 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
callback = where; callback = where;
where = undefined; where = undefined;
} }
var cache = this.cache[model]; var cache = this.collection(model);
var filter = null; var filter = null;
if (where) { if (where) {
filter = applyFilter({where: where}); filter = applyFilter({where: where});
} Object.keys(cache).forEach(function (id) {
Object.keys(cache).forEach(function (id) { if (!filter || filter(this.fromDb(model, cache[id]))) {
if (!filter || filter(this.fromDb(model, cache[id]))) { delete cache[id];
delete cache[id]; }
} }.bind(this));
}.bind(this)); } else {
if (!where) { this.collection(model, {});
this.cache[model] = {};
} }
this.saveToFile(null, callback); this.saveToFile(null, callback);
}; };
Memory.prototype.count = function count(model, callback, where) { Memory.prototype.count = function count(model, callback, where) {
var cache = this.cache[model]; var cache = this.collection(model);
var data = Object.keys(cache); var data = Object.keys(cache);
if (where) { if (where) {
var filter = {where: where}; var filter = {where: where};
@ -539,7 +558,7 @@ Memory.prototype.count = function count(model, callback, where) {
Memory.prototype.update = Memory.prototype.update =
Memory.prototype.updateAll = function updateAll(model, where, data, cb) { Memory.prototype.updateAll = function updateAll(model, where, data, cb) {
var self = this; var self = this;
var cache = this.cache[model]; var cache = this.collection(model);
var filter = null; var filter = null;
where = where || {}; where = where || {};
filter = applyFilter({where: where}); filter = applyFilter({where: where});
@ -571,8 +590,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
this.setIdValue(model, data, id); this.setIdValue(model, data, id);
var cachedModels = this.cache[model]; var cachedModels = this.collection(model);
var modelData = cachedModels && this.cache[model][id]; var modelData = cachedModels && this.collection(model)[id];
if (modelData) { if (modelData) {
this.save(model, data, cb); this.save(model, data, cb);
@ -594,6 +613,16 @@ Memory.prototype.buildNearFilter = function (filter) {
// noop // noop
} }
Memory.prototype.automigrate = function (models, cb) {
if (typeof models === 'function') cb = models, models = [];
if (models.length === 0) models = Object.keys(this._models);
var self = this;
models.forEach(function(m) {
self.initCollection(m);
});
if (cb) cb();
}
function merge(base, update) { function merge(base, update) {
if (!base) { if (!base) {
return update; return update;

View File

@ -13,11 +13,12 @@ var Relation = require('./relations.js');
var Inclusion = require('./include.js'); var Inclusion = require('./include.js');
var List = require('./list.js'); var List = require('./list.js');
var geo = require('./geo'); var geo = require('./geo');
var mergeQuery = require('./scope.js').mergeQuery;
var Memory = require('./connectors/memory').Memory; var Memory = require('./connectors/memory').Memory;
var utils = require('./utils'); var utils = require('./utils');
var fieldsToArray = utils.fieldsToArray; var fieldsToArray = utils.fieldsToArray;
var removeUndefined = utils.removeUndefined; var removeUndefined = utils.removeUndefined;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery;
var util = require('util'); var util = require('util');
var assert = require('assert'); var assert = require('assert');
@ -53,6 +54,14 @@ function setIdValue(m, data, value) {
} }
} }
function byIdQuery(m, id) {
var pk = idName(m);
var query = { where: {} };
query.where[pk] = id;
m.applyScope(query);
return query;
}
DataAccessObject._forDB = function (data) { DataAccessObject._forDB = function (data) {
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) { if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
return data; return data;
@ -69,6 +78,40 @@ DataAccessObject._forDB = function (data) {
return res; return res;
}; };
DataAccessObject.defaultScope = function(target, inst) {
var scope = this.definition.settings.scope;
if (typeof scope === 'function') {
scope = this.definition.settings.scope.call(this, target, inst);
}
return scope;
};
DataAccessObject.applyScope = function(query, inst) {
var scope = this.defaultScope(query, inst) || {};
if (typeof scope === 'object') {
mergeQuery(query, scope || {}, this.definition.settings.scoping);
}
};
DataAccessObject.applyProperties = function(data, inst) {
var properties = this.definition.settings.properties;
properties = properties || this.definition.settings.attributes;
if (typeof properties === 'object') {
util._extend(data, properties);
} else if (typeof properties === 'function') {
util._extend(data, properties.call(this, data, inst) || {});
} else if (properties !== false) {
var scope = this.defaultScope(data, inst) || {};
if (typeof scope.where === 'object') {
setScopeValuesFromWhere(data, scope.where, this);
}
}
};
DataAccessObject.lookupModel = function(data) {
return this;
};
/** /**
* Create an instance of Model with given data and save to the attached data source. Callback is optional. * Create an instance of Model with given data and save to the attached data source. Callback is optional.
* Example: * Example:
@ -89,8 +132,8 @@ DataAccessObject.create = function (data, callback) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this; var Model = this;
var modelName = Model.modelName; var self = this;
if (typeof data === 'function') { if (typeof data === 'function') {
callback = data; callback = data;
data = {}; data = {};
@ -116,6 +159,7 @@ DataAccessObject.create = function (data, callback) {
for (var i = 0; i < data.length; i += 1) { for (var i = 0; i < data.length; i += 1) {
(function (d, i) { (function (d, i) {
Model = self.lookupModel(d); // data-specific
instances.push(Model.create(d, function (err, inst) { instances.push(Model.create(d, function (err, inst) {
if (err) { if (err) {
errors[i] = err; errors[i] = err;
@ -131,11 +175,16 @@ DataAccessObject.create = function (data, callback) {
function modelCreated() { function modelCreated() {
if (--wait === 0) { if (--wait === 0) {
callback(gotError ? errors : null, instances); callback(gotError ? errors : null, instances);
if(!gotError) instances.forEach(Model.emit.bind('changed')); if(!gotError) {
instances.forEach(function(inst) {
inst.constructor.emit('changed');
});
}
} }
} }
} }
var enforced = {};
var obj; var obj;
var idValue = getIdValue(this, data); var idValue = getIdValue(this, data);
@ -145,6 +194,13 @@ DataAccessObject.create = function (data, callback) {
} else { } else {
obj = new Model(data); obj = new Model(data);
} }
this.applyProperties(enforced, obj);
obj.setAttributes(enforced);
Model = this.lookupModel(data); // data-specific
if (Model !== obj.constructor) obj = new Model(data);
data = obj.toObject(true); data = obj.toObject(true);
// validation required // validation required
@ -155,12 +211,13 @@ DataAccessObject.create = function (data, callback) {
callback(new ValidationError(obj), obj); callback(new ValidationError(obj), obj);
} }
}, data); }, data);
function create() { function create() {
obj.trigger('create', function (createDone) { obj.trigger('create', function (createDone) {
obj.trigger('save', function (saveDone) { obj.trigger('save', function (saveDone) {
var _idName = idName(Model); var _idName = idName(Model);
var modelName = Model.modelName;
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) { this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
if (id) { if (id) {
obj.__data[_idName] = id; obj.__data[_idName] = id;
@ -208,7 +265,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
if (stillConnecting(this.getDataSource(), this, arguments)) { if (stillConnecting(this.getDataSource(), this, arguments)) {
return; return;
} }
var self = this;
var Model = this; var Model = this;
if (!getIdValue(this, data)) { if (!getIdValue(this, data)) {
return this.create(data, callback); return this.create(data, callback);
@ -220,7 +277,9 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
inst = new Model(data); inst = new Model(data);
} }
update = inst.toObject(false); update = inst.toObject(false);
this.applyProperties(update, inst);
update = removeUndefined(update); update = removeUndefined(update);
Model = this.lookupModel(update);
this.getDataSource().connector.updateOrCreate(Model.modelName, update, function (err, data) { this.getDataSource().connector.updateOrCreate(Model.modelName, update, function (err, data) {
var obj; var obj;
if (data && !(data instanceof Model)) { if (data && !(data instanceof Model)) {
@ -242,6 +301,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
if (inst) { if (inst) {
inst.updateAttributes(data, callback); inst.updateAttributes(data, callback);
} else { } else {
Model = self.lookupModel(data);
var obj = new Model(data); var obj = new Model(data);
obj.save(data, callback); obj.save(data, callback);
} }
@ -290,7 +350,9 @@ DataAccessObject.exists = function exists(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
if (id !== undefined && id !== null && id !== '') { if (id !== undefined && id !== null && id !== '') {
this.dataSource.connector.exists(this.modelName, id, cb); this.count(byIdQuery(this, id).where, function(err, count) {
cb(err, err ? false : count === 1);
});
} else { } else {
cb(new Error('Model::exists requires the id argument')); cb(new Error('Model::exists requires the id argument'));
} }
@ -311,17 +373,7 @@ DataAccessObject.exists = function exists(id, cb) {
*/ */
DataAccessObject.findById = function find(id, cb) { DataAccessObject.findById = function find(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
this.findOne(byIdQuery(this, id), cb);
this.getDataSource().connector.find(this.modelName, id, function (err, data) {
var obj = null;
if (data) {
if (!getIdValue(this, data)) {
setIdValue(this, data, id);
}
obj = new this(data, {applySetters: false, persisted: true});
}
cb(err, obj);
}.bind(this));
}; };
DataAccessObject.findByIds = function(ids, cond, cb) { DataAccessObject.findByIds = function(ids, cond, cb) {
@ -683,7 +735,7 @@ DataAccessObject.find = function find(query, cb) {
var self = this; var self = this;
query = query || {}; query = query || {};
try { try {
this._normalize(query); this._normalize(query);
} catch (err) { } catch (err) {
@ -692,6 +744,8 @@ DataAccessObject.find = function find(query, cb) {
}); });
} }
this.applyScope(query);
var near = query && geo.nearFilter(query.where); var near = query && geo.nearFilter(query.where);
var supportsGeo = !!this.getDataSource().connector.buildNearFilter; var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
@ -702,6 +756,7 @@ DataAccessObject.find = function find(query, cb) {
} else if (query.where) { } else if (query.where) {
// do in memory query // do in memory query
// using all documents // using all documents
// TODO [fabien] use default scope here?
this.getDataSource().connector.all(this.modelName, {}, function (err, data) { this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
var memory = new Memory(); var memory = new Memory();
var modelName = self.modelName; var modelName = self.modelName;
@ -735,8 +790,9 @@ DataAccessObject.find = function find(query, cb) {
this.getDataSource().connector.all(this.modelName, query, function (err, data) { this.getDataSource().connector.all(this.modelName, query, function (err, data) {
if (data && data.forEach) { if (data && data.forEach) {
data.forEach(function (d, i) { data.forEach(function (d, i) {
var obj = new self(d, {fields: query.fields, applySetters: false, persisted: true}); var Model = self.lookupModel(d);
var obj = new Model(d, {fields: query.fields, applySetters: false, persisted: true});
if (query && query.include) { if (query && query.include) {
if (query.collect) { if (query.collect) {
// The collect property indicates that the query is to return the // The collect property indicates that the query is to return the
@ -747,19 +803,19 @@ DataAccessObject.find = function find(query, cb) {
// This handles the case to return parent items including the related // This handles the case to return parent items including the related
// models. For example, Article.find({include: 'tags'}, ...); // models. For example, Article.find({include: 'tags'}, ...);
// Try to normalize the include // Try to normalize the include
var includes = query.include || []; var includes = Inclusion.normalizeInclude(query.include || []);
if (typeof includes === 'string') {
includes = [includes];
} else if (!Array.isArray(includes) && typeof includes === 'object') {
includes = Object.keys(includes);
}
includes.forEach(function (inc) { includes.forEach(function (inc) {
var relationName = inc;
if (utils.isPlainObject(inc)) {
relationName = Object.keys(inc)[0];
}
// Promote the included model as a direct property // Promote the included model as a direct property
var data = obj.__cachedRelations[inc]; var data = obj.__cachedRelations[relationName];
if(Array.isArray(data)) { if(Array.isArray(data)) {
data = new List(data, null, obj); data = new List(data, null, obj);
} }
obj.__data[inc] = data; if (data) obj.__data[relationName] = data;
}); });
delete obj.__data.__cachedRelations; delete obj.__data.__cachedRelations;
} }
@ -817,34 +873,39 @@ DataAccessObject.findOne = function findOne(query, cb) {
* @param {Function} [cb] Callback called with (err) * @param {Function} [cb] Callback called with (err)
*/ */
DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) { DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyAll = function destroyAll(where, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this; var Model = this;
if (!cb && 'function' === typeof where) { if (!cb && 'function' === typeof where) {
cb = where; cb = where;
where = undefined; where = undefined;
} }
if (!where) {
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) { var query = { where: where };
cb && cb(err, data); this.applyScope(query);
if(!err) Model.emit('deletedAll'); where = query.where;
}.bind(this));
} else { if (!where || (typeof where === 'object' && Object.keys(where).length === 0)) {
try { this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
// Support an optional where object cb && cb(err, data);
where = removeUndefined(where); if(!err) Model.emit('deletedAll');
where = this._coerce(where); }.bind(this));
} catch (err) { } else {
return process.nextTick(function() { try {
cb && cb(err); // Support an optional where object
}); where = removeUndefined(where);
} where = this._coerce(where);
this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { } catch (err) {
cb && cb(err, data); return process.nextTick(function() {
if(!err) Model.emit('deletedAll', where); cb && cb(err);
}.bind(this)); });
} }
}; this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) {
cb && cb(err, data);
if(!err) Model.emit('deletedAll', where);
}.bind(this));
}
};
/** /**
* Delete the record with the specified ID. * Delete the record with the specified ID.
@ -857,16 +918,16 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
// 'deleteById' will be used as the name for strong-remoting to keep it backward // 'deleteById' will be used as the name for strong-remoting to keep it backward
// compatible for angular SDK // compatible for angular SDK
DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) { DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.deleteById = function deleteById(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this; var Model = this;
this.getDataSource().connector.destroy(this.modelName, id, function (err) { this.remove(byIdQuery(this, id).where, function(err) {
if ('function' === typeof cb) { if ('function' === typeof cb) {
cb(err); cb(err);
} }
if(!err) Model.emit('deleted', id); if(!err) Model.emit('deleted', id);
}.bind(this)); });
}; };
/** /**
* Return count of matched records. Optional query parameter allows you to count filtered set of model instances. * Return count of matched records. Optional query parameter allows you to count filtered set of model instances.
@ -888,6 +949,11 @@ DataAccessObject.count = function (where, cb) {
cb = where; cb = where;
where = null; where = null;
} }
var query = { where: where };
this.applyScope(query);
where = query.where;
try { try {
where = removeUndefined(where); where = removeUndefined(where);
where = this._coerce(where); where = this._coerce(where);
@ -896,6 +962,7 @@ DataAccessObject.count = function (where, cb) {
cb && cb(err); cb && cb(err);
}); });
} }
this.getDataSource().connector.count(this.modelName, cb, where); this.getDataSource().connector.count(this.modelName, cb, where);
}; };
@ -926,13 +993,17 @@ DataAccessObject.prototype.save = function (options, callback) {
if (!('throws' in options)) { if (!('throws' in options)) {
options.throws = false; options.throws = false;
} }
var inst = this; var inst = this;
var data = inst.toObject(true); var data = inst.toObject(true);
var modelName = Model.modelName; var modelName = Model.modelName;
Model.applyProperties(data, this);
if (this.isNewRecord()) { if (this.isNewRecord()) {
return Model.create(this, callback); return Model.create(this, callback);
} else {
inst.setAttributes(data);
} }
// validate first // validate first
@ -1016,7 +1087,13 @@ DataAccessObject.updateAll = function (where, data, cb) {
assert(typeof where === 'object', 'The where argument should be an object'); assert(typeof where === 'object', 'The where argument should be an object');
assert(typeof data === 'object', 'The data argument should be an object'); assert(typeof data === 'object', 'The data argument should be an object');
assert(cb === null || typeof cb === 'function', 'The cb argument should be a function'); assert(cb === null || typeof cb === 'function', 'The cb argument should be a function');
var query = { where: where };
this.applyScope(query);
this.applyProperties(data);
where = query.where;
try { try {
where = removeUndefined(where); where = removeUndefined(where);
where = this._coerce(where); where = this._coerce(where);
@ -1025,6 +1102,7 @@ DataAccessObject.updateAll = function (where, data, cb) {
cb && cb(err); cb && cb(err);
}); });
} }
var connector = this.getDataSource().connector; var connector = this.getDataSource().connector;
connector.update(this.modelName, where, data, cb); connector.update(this.modelName, where, data, cb);
}; };
@ -1075,7 +1153,7 @@ DataAccessObject.prototype.remove =
* @param {Mixed} value Value of property * @param {Mixed} value Value of property
*/ */
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) { DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
this[name] = value; this[name] = value; // TODO [fabien] - currently not protected by applyProperties
}; };
/** /**
@ -1101,6 +1179,8 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu
DataAccessObject.prototype.setAttributes = function setAttributes(data) { DataAccessObject.prototype.setAttributes = function setAttributes(data) {
if (typeof data !== 'object') return; if (typeof data !== 'object') return;
this.constructor.applyProperties(data, this);
var Model = this.constructor; var Model = this.constructor;
var inst = this; var inst = this;

View File

@ -1427,6 +1427,7 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) {
*/ */
DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) { DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) {
var self = this; var self = this;
options = options || {};
this.discoverSchemas(modelName, options, function (err, schemas) { this.discoverSchemas(modelName, options, function (err, schemas) {
if (err) { if (err) {
cb && cb(err, schemas); cb && cb(err, schemas);
@ -1436,14 +1437,16 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb)
var schemaList = []; var schemaList = [];
for (var s in schemas) { for (var s in schemas) {
var schema = schemas[s]; var schema = schemas[s];
if (options.base) {
schema.options = schema.options || {};
schema.options.base = options.base;
}
schemaList.push(schema); schemaList.push(schema);
} }
var models = self.modelBuilder.buildModels(schemaList); var models = self.modelBuilder.buildModels(schemaList,
// Now attach the models to the data source self.createModel.bind(self));
for (var m in models) {
models[m].attachTo(self);
}
cb && cb(err, models); cb && cb(err, models);
}); });
}; };
@ -1462,18 +1465,41 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb)
* @param {Object} [options] The options * @param {Object} [options] The options
*/ */
DataSource.prototype.discoverAndBuildModelsSync = function (modelName, options) { DataSource.prototype.discoverAndBuildModelsSync = function (modelName, options) {
options = options || {};
var schemas = this.discoverSchemasSync(modelName, options); var schemas = this.discoverSchemasSync(modelName, options);
var schemaList = []; var schemaList = [];
for (var s in schemas) { for (var s in schemas) {
var schema = schemas[s]; var schema = schemas[s];
if (options.base) {
schema.options = schema.options || {};
schema.options.base = options.base;
}
schemaList.push(schema); schemaList.push(schema);
} }
var models = this.modelBuilder.buildModels(schemaList); var models = this.modelBuilder.buildModels(schemaList,
this.createModel.bind(this));
return models; return models;
}; };
/**
* Introspect a JSON object and build a model class
* @param {String} name Name of the model
* @param {Object} json The json object representing a model instance
* @param {Object} options Options
* @returns {*}
*/
DataSource.prototype.buildModelFromInstance = function (name, json, options) {
// Introspect the JSON document to generate a schema
var schema = ModelBuilder.introspect(json);
// Create a model for the generated schema
return this.createModel(name, schema, options);
};
/** /**
* Check whether migrations needed * Check whether migrations needed
* This method applies only to SQL connectors. * This method applies only to SQL connectors.

View File

@ -3,6 +3,53 @@ var utils = require('./utils');
var isPlainObject = utils.isPlainObject; var isPlainObject = utils.isPlainObject;
var defineCachedRelations = utils.defineCachedRelations; var defineCachedRelations = utils.defineCachedRelations;
/*!
* Normalize the include to be an array
* @param include
* @returns {*}
*/
function normalizeInclude(include) {
if (typeof include === 'string') {
return [include];
} else if (isPlainObject(include)) {
// Build an array of key/value pairs
var newInclude = [];
var rel = include.rel || include.relation;
if (typeof rel === 'string') {
var obj = {};
obj[rel] = new IncludeScope(include.scope);
newInclude.push(obj);
} else {
for (var key in include) {
var obj = {};
obj[key] = include[key];
newInclude.push(obj);
}
}
return newInclude;
} else {
return include;
}
}
function IncludeScope(scope) {
this._scope = utils.deepMerge({}, scope || {});
if (this._scope.include) {
this._include = normalizeInclude(this._scope.include);
delete this._scope.include;
} else {
this._include = null;
}
};
IncludeScope.prototype.conditions = function() {
return utils.deepMerge({}, this._scope);
};
IncludeScope.prototype.include = function() {
return this._include;
};
/*! /*!
* Include mixin for ./model.js * Include mixin for ./model.js
*/ */
@ -17,6 +64,13 @@ module.exports = Inclusion;
function Inclusion() { function Inclusion() {
} }
/**
* Normalize includes - used in DataAccessObject
*
*/
Inclusion.normalizeInclude = normalizeInclude;
/** /**
* Enables you to load relations of several objects and optimize numbers of requests. * Enables you to load relations of several objects and optimize numbers of requests.
* *
@ -52,49 +106,33 @@ Inclusion.include = function (objects, include, cb) {
} }
include = normalizeInclude(include); include = normalizeInclude(include);
async.each(include, function(item, callback) { async.each(include, function(item, callback) {
processIncludeItem(objects, item, callback); processIncludeItem(objects, item, callback);
}, function(err) { }, function(err) {
cb && cb(err, objects); cb && cb(err, objects);
}); });
/*!
* Normalize the include to be an array
* @param include
* @returns {*}
*/
function normalizeInclude(include) {
if (typeof include === 'string') {
return [include];
} else if (isPlainObject(include)) {
// Build an array of key/value pairs
var newInclude = [];
for (var key in include) {
var obj = {};
obj[key] = include[key];
newInclude.push(obj);
}
return newInclude;
} else {
return include;
}
}
function processIncludeItem(objs, include, cb) { function processIncludeItem(objs, include, cb) {
var relations = self.relations; var relations = self.relations;
var relationName, subInclude; var relationName;
var subInclude = null, scope = null;
if (isPlainObject(include)) { if (isPlainObject(include)) {
relationName = Object.keys(include)[0]; relationName = Object.keys(include)[0];
subInclude = include[relationName]; if (include[relationName] instanceof IncludeScope) {
scope = include[relationName];
subInclude = scope.include();
} else {
subInclude = include[relationName];
}
} else { } else {
relationName = include; relationName = include;
subInclude = null; subInclude = null;
} }
var relation = relations[relationName];
var relation = relations[relationName];
if (!relation) { if (!relation) {
cb(new Error('Relation "' + relationName + '" is not defined for ' cb(new Error('Relation "' + relationName + '" is not defined for '
+ self.modelName + ' model')); + self.modelName + ' model'));
@ -126,10 +164,35 @@ Inclusion.include = function (objects, include, cb) {
var inst = (obj instanceof self) ? obj : new self(obj); var inst = (obj instanceof self) ? obj : new self(obj);
// Calling the relation method on the instance // Calling the relation method on the instance
inst[relationName](function (err, result) {
var related; // relation accessor function
if (relation.multiple && 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 = [];
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});
related = inst[relationName].bind(inst, filter);
} else {
related = inst[relationName].bind(inst);
}
related(function (err, result) {
if (err) { if (err) {
return callback(err); return callback(err);
} else { } else {
defineCachedRelations(obj); defineCachedRelations(obj);
obj.__cachedRelations[relationName] = result; obj.__cachedRelations[relationName] = result;

View File

@ -24,12 +24,13 @@ module.exports = function getIntrospector(ModelBuilder) {
return 'date'; return 'date';
} }
var itemType;
if (Array.isArray(value)) { if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) { for (var i = 0; i < value.length; i++) {
if (value[i] === null || value[i] === undefined) { if (value[i] === null || value[i] === undefined) {
continue; continue;
} }
var itemType = introspectType(value[i]); itemType = introspectType(value[i]);
if (itemType) { if (itemType) {
return [itemType]; return [itemType];
} }
@ -43,7 +44,7 @@ module.exports = function getIntrospector(ModelBuilder) {
var properties = {}; var properties = {};
for (var p in value) { for (var p in value) {
var itemType = introspectType(value[p]); itemType = introspectType(value[p]);
if (itemType) { if (itemType) {
properties[p] = itemType; properties[p] = itemType;
} }
@ -54,6 +55,7 @@ module.exports = function getIntrospector(ModelBuilder) {
return properties; return properties;
} }
ModelBuilder.introspect = introspectType;
return introspectType; return introspectType;
} }

View File

@ -140,6 +140,10 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
var ModelBaseClass = parent || this.defaultModelBaseClass; var ModelBaseClass = parent || this.defaultModelBaseClass;
var baseClass = settings.base || settings['super']; var baseClass = settings.base || settings['super'];
if (baseClass) { if (baseClass) {
// Normalize base model property
settings.base = baseClass;
delete settings['super'];
if (isModelClass(baseClass)) { if (isModelClass(baseClass)) {
ModelBaseClass = baseClass; ModelBaseClass = baseClass;
} else { } else {
@ -337,8 +341,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
} }
// Merge the settings // Merge the settings
var originalSubclassSettings = subclassSettings;
subclassSettings = mergeSettings(settings, subclassSettings); subclassSettings = mergeSettings(settings, subclassSettings);
// Ensure 'base' is not inherited. Note we don't have to delete 'super'
// as that is removed from settings by modelBuilder.define and thus
// it is never inherited
if (!originalSubclassSettings.base)
delete subclassSettings.base;
// Define the subclass // Define the subclass
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass); var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
@ -632,7 +643,7 @@ ModelBuilder.prototype.resolveType = function (type) {
* @param {*} schemas The schemas * @param {*} schemas The schemas
* @returns {Object} A map of model constructors keyed by model name * @returns {Object} A map of model constructors keyed by model name
*/ */
ModelBuilder.prototype.buildModels = function (schemas) { ModelBuilder.prototype.buildModels = function (schemas, createModel) {
var models = {}; var models = {};
// Normalize the schemas to be an array of the schema objects {name: <name>, properties: {}, options: {}} // Normalize the schemas to be an array of the schema objects {name: <name>, properties: {}, options: {}}
@ -656,7 +667,12 @@ ModelBuilder.prototype.buildModels = function (schemas) {
for (var s in schemas) { for (var s in schemas) {
var name = this.getSchemaName(schemas[s].name); var name = this.getSchemaName(schemas[s].name);
schemas[s].name = name; schemas[s].name = name;
var model = this.define(schemas[s].name, schemas[s].properties, schemas[s].options); var model;
if(typeof createModel === 'function') {
model = createModel(schemas[s].name, schemas[s].properties, schemas[s].options);
} else {
model = this.define(schemas[s].name, schemas[s].properties, schemas[s].options);
}
models[name] = model; models[name] = model;
relations = relations.concat(model.definition.relations); relations = relations.concat(model.definition.relations);
} }

View File

@ -60,6 +60,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
} }
var properties = _extend({}, ctor.definition.properties); var properties = _extend({}, ctor.definition.properties);
data = data || {}; data = data || {};
if (typeof ctor.applyProperties === 'function') {
ctor.applyProperties(data);
}
options = options || {}; options = options || {};
var applySetters = options.applySetters; var applySetters = options.applySetters;
@ -130,7 +134,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
} }
if (properties[p]) { if (properties[p]) {
// Managed property // Managed property
if (applySetters) { if (applySetters || properties[p].id) {
self[p] = propVal; self[p] = propVal;
} else { } else {
self.__data[p] = propVal; self.__data[p] = propVal;

View File

@ -3,9 +3,10 @@
*/ */
var assert = require('assert'); var assert = require('assert');
var util = require('util'); var util = require('util');
var utils = require('./utils');
var i8n = require('inflection'); var i8n = require('inflection');
var defineScope = require('./scope.js').defineScope; var defineScope = require('./scope.js').defineScope;
var mergeQuery = require('./scope.js').mergeQuery; var mergeQuery = utils.mergeQuery;
var ModelBaseClass = require('./model.js'); var ModelBaseClass = require('./model.js');
var applyFilter = require('./connectors/memory').applyFilter; var applyFilter = require('./connectors/memory').applyFilter;
var ValidationError = require('./validations.js').ValidationError; var ValidationError = require('./validations.js').ValidationError;

View File

@ -1,13 +1,14 @@
var i8n = require('inflection'); var i8n = require('inflection');
var utils = require('./utils'); var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations; var defineCachedRelations = utils.defineCachedRelations;
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
var mergeQuery = utils.mergeQuery;
var DefaultModelBaseClass = require('./model.js'); var DefaultModelBaseClass = require('./model.js');
/** /**
* Module exports * Module exports
*/ */
exports.defineScope = defineScope; exports.defineScope = defineScope;
exports.mergeQuery = mergeQuery;
function ScopeDefinition(definition) { function ScopeDefinition(definition) {
this.isStatic = definition.isStatic; this.isStatic = definition.isStatic;
@ -168,7 +169,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
f._targetClass = targetModel.modelName; f._targetClass = targetModel.modelName;
if (f._scope.collect) { if (f._scope.collect) {
f._targetClass = i8n.capitalize(f._scope.collect); f._targetClass = i8n.camelize(f._scope.collect);
} }
f.build = build; f.build = build;
@ -229,35 +230,6 @@ function defineScope(cls, targetClass, name, params, methods, options) {
cls['__count__' + name] = fn_count; cls['__count__' + name] = fn_count;
/*
* Extracting fixed property values for the scope from the where clause into
* the data object
*
* @param {Object} The data object
* @param {Object} The where clause
*/
function setScopeValuesFromWhere(data, where, targetModel) {
for (var i in where) {
if (i === 'and') {
// Find fixed property values from each subclauses
for (var w = 0, n = where[i].length; w < n; w++) {
setScopeValuesFromWhere(data, where[i][w], targetModel);
}
continue;
}
var prop = targetModel.definition.properties[i];
if (prop) {
var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type
|| prop.type.name === 'ObjectID') // MongoDB key
{
// Only pick the {propertyName: propertyValue}
data[i] = where[i];
}
}
}
}
// and it should have create/build methods with binded thisModelNameId param // and it should have create/build methods with binded thisModelNameId param
function build(data) { function build(data) {
data = data || {}; data = data || {};
@ -300,60 +272,3 @@ function defineScope(cls, targetClass, name, params, methods, options) {
return definition; return definition;
} }
/*!
* Merge query parameters
* @param {Object} base The base object to contain the merged results
* @param {Object} update The object containing updates to be merged
* @returns {*|Object} The base object
* @private
*/
function mergeQuery(base, update) {
if (!update) {
return;
}
base = base || {};
if (update.where && Object.keys(update.where).length > 0) {
if (base.where && Object.keys(base.where).length > 0) {
base.where = {and: [base.where, update.where]};
} else {
base.where = update.where;
}
}
// Merge inclusion
if (update.include) {
if (!base.include) {
base.include = update.include;
} else {
var saved = base.include;
base.include = {};
base.include[update.include] = saved;
}
}
if (update.collect) {
base.collect = update.collect;
}
// set order
if (!base.order && update.order) {
base.order = update.order;
}
// overwrite pagination
if (update.limit !== undefined) {
base.limit = update.limit;
}
if (update.skip !== undefined) {
base.skip = update.skip;
}
if (update.offset !== undefined) {
base.offset = update.offset;
}
// Overwrite fields
if (update.fields !== undefined) {
base.fields = update.fields;
}
return base;
}

View File

@ -3,10 +3,12 @@ exports.fieldsToArray = fieldsToArray;
exports.selectFields = selectFields; exports.selectFields = selectFields;
exports.removeUndefined = removeUndefined; exports.removeUndefined = removeUndefined;
exports.parseSettings = parseSettings; exports.parseSettings = parseSettings;
exports.mergeSettings = mergeSettings; exports.mergeSettings = exports.deepMerge = mergeSettings;
exports.isPlainObject = isPlainObject; exports.isPlainObject = isPlainObject;
exports.defineCachedRelations = defineCachedRelations; exports.defineCachedRelations = defineCachedRelations;
exports.sortObjectsByIds = sortObjectsByIds; exports.sortObjectsByIds = sortObjectsByIds;
exports.setScopeValuesFromWhere = setScopeValuesFromWhere;
exports.mergeQuery = mergeQuery;
var traverse = require('traverse'); var traverse = require('traverse');
@ -21,6 +23,103 @@ function safeRequire(module) {
} }
} }
/*
* Extracting fixed property values for the scope from the where clause into
* the data object
*
* @param {Object} The data object
* @param {Object} The where clause
*/
function setScopeValuesFromWhere(data, where, targetModel) {
for (var i in where) {
if (i === 'and') {
// Find fixed property values from each subclauses
for (var w = 0, n = where[i].length; w < n; w++) {
setScopeValuesFromWhere(data, where[i][w], targetModel);
}
continue;
}
var prop = targetModel.definition.properties[i];
if (prop) {
var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type
|| prop.type.name === 'ObjectID') // MongoDB key
{
// Only pick the {propertyName: propertyValue}
data[i] = where[i];
}
}
}
}
/*!
* Merge query parameters
* @param {Object} base The base object to contain the merged results
* @param {Object} update The object containing updates to be merged
* @param {Object} spec Optionally specifies parameters to exclude (set to false)
* @returns {*|Object} The base object
* @private
*/
function mergeQuery(base, update, spec) {
if (!update) {
return;
}
spec = spec || {};
base = base || {};
if (update.where && Object.keys(update.where).length > 0) {
if (base.where && Object.keys(base.where).length > 0) {
base.where = {and: [base.where, update.where]};
} else {
base.where = update.where;
}
}
// Merge inclusion
if (spec.include !== false && update.include) {
if (!base.include) {
base.include = update.include;
} else {
var saved = base.include;
base.include = {};
base.include[update.include] = saved;
}
}
if (spec.collect !== false && update.collect) {
base.collect = update.collect;
}
// Overwrite fields
if (spec.fields !== false && update.fields !== undefined) {
base.fields = update.fields;
} else if (update.fields !== undefined) {
base.fields = [].concat(base.fields).concat(update.fields);
}
// set order
if ((!base.order || spec.order === false) && update.order) {
base.order = update.order;
}
// overwrite pagination
if (spec.limit !== false && update.limit !== undefined) {
base.limit = update.limit;
}
var skip = spec.skip !== false && spec.offset !== false;
if (skip && update.skip !== undefined) {
base.skip = update.skip;
}
if (skip && update.offset !== undefined) {
base.offset = update.offset;
}
return base;
}
function fieldsToArray(fields, properties) { function fieldsToArray(fields, properties) {
if (!fields) return; if (!fields) return;

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-datasource-juggler", "name": "loopback-datasource-juggler",
"version": "2.9.0", "version": "2.10.0",
"description": "LoopBack DataSoure Juggler", "description": "LoopBack DataSoure Juggler",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",

808
test/default-scope.test.js Normal file
View File

@ -0,0 +1,808 @@
// This test written in mocha+should.js
var should = require('./init.js');
var async = require('async');
var db, Category, Product, Tool, Widget, Thing;
// This test requires a connector that can
// handle a custom collection or table name
// TODO [fabien] add table for pgsql/mysql
// TODO [fabien] change model definition - see #293
var setupProducts = function(ids, done) {
async.series([
function(next) {
Tool.create({name: 'Tool Z'}, function(err, inst) {
ids.toolZ = inst.id;
next();
});
},
function(next) {
Widget.create({name: 'Widget Z'}, function(err, inst) {
ids.widgetZ = inst.id;
next();
});
},
function(next) {
Tool.create({name: 'Tool A', active: false}, function(err, inst) {
ids.toolA = inst.id;
next();
});
},
function(next) {
Widget.create({name: 'Widget A'}, function(err, inst) {
ids.widgetA = inst.id;
next();
});
},
function(next) {
Widget.create({name: 'Widget B', active: false}, function(err, inst) {
ids.widgetB = inst.id;
next();
});
}
], done);
};
describe('default scope', function () {
before(function (done) {
db = getSchema();
Category = db.define('Category', {
name: String
});
Product = db.define('Product', {
name: String,
kind: String,
description: String,
active: { type: Boolean, default: true }
}, {
scope: { order: 'name' },
scopes: { active: { where: { active: true } } }
});
Product.lookupModel = function(data) {
var m = this.dataSource.models[data.kind];
if (m.base === this) return m;
return this;
};
Tool = db.define('Tool', Product.definition.properties, {
base: 'Product',
scope: { where: { kind: 'Tool' }, order: 'name' },
scopes: { active: { where: { active: true } } },
mongodb: { collection: 'Product' },
memory: { collection: 'Product' }
});
Widget = db.define('Widget', Product.definition.properties, {
base: 'Product',
properties: { kind: 'Widget' },
scope: { where: { kind: 'Widget' }, order: 'name' },
scopes: { active: { where: { active: true } } },
mongodb: { collection: 'Product' },
memory: { collection: 'Product' }
});
// inst is only valid for instance methods
// like save, updateAttributes
var scopeFn = function(target, inst) {
return { where: { kind: this.modelName } };
};
var propertiesFn = function(target, inst) {
return { kind: this.modelName };
};
Thing = db.define('Thing', Product.definition.properties, {
base: 'Product',
attributes: propertiesFn,
scope: scopeFn,
mongodb: { collection: 'Product' },
memory: { collection: 'Product' }
});
Category.hasMany(Product);
Category.hasMany(Tool, {scope: {order: 'name DESC'}});
Category.hasMany(Widget);
Category.hasMany(Thing);
Product.belongsTo(Category);
Tool.belongsTo(Category);
Widget.belongsTo(Category);
Thing.belongsTo(Category);
db.automigrate(done);
});
describe('manipulation', function() {
var ids = {};
before(function(done) {
db.automigrate(done);
});
it('should return a scoped instance', function() {
var p = new Tool({name: 'Product A', kind:'ignored'});
p.name.should.equal('Product A');
p.kind.should.equal('Tool');
p.setAttributes({ kind: 'ignored' });
p.kind.should.equal('Tool');
p.setAttribute('kind', 'other'); // currently not enforced
p.kind.should.equal('other');
});
it('should create a scoped instance - tool', function(done) {
Tool.create({name: 'Product A', kind: 'ignored'}, function(err, p) {
should.not.exist(err);
p.name.should.equal('Product A');
p.kind.should.equal('Tool');
ids.productA = p.id;
done();
});
});
it('should create a scoped instance - widget', function(done) {
Widget.create({name: 'Product B', kind: 'ignored'}, function(err, p) {
should.not.exist(err);
p.name.should.equal('Product B');
p.kind.should.equal('Widget');
ids.productB = p.id;
done();
});
});
it('should update a scoped instance - updateAttributes', function(done) {
Tool.findById(ids.productA, function(err, p) {
p.updateAttributes({description: 'A thing...', kind: 'ingored'}, function(err, inst) {
should.not.exist(err);
p.name.should.equal('Product A');
p.kind.should.equal('Tool');
p.description.should.equal('A thing...');
done();
});
});
});
it('should update a scoped instance - save', function(done) {
Tool.findById(ids.productA, function(err, p) {
p.description = 'Something...';
p.kind = 'ignored';
p.save(function(err, inst) {
should.not.exist(err);
p.name.should.equal('Product A');
p.kind.should.equal('Tool');
p.description.should.equal('Something...');
Tool.findById(ids.productA, function(err, p) {
p.kind.should.equal('Tool');
done();
});
});
});
});
it('should update a scoped instance - updateOrCreate', function(done) {
var data = {id: ids.productA, description: 'Anything...', kind: 'ingored'};
Tool.updateOrCreate(data, function(err, p) {
should.not.exist(err);
p.name.should.equal('Product A');
p.kind.should.equal('Tool');
p.description.should.equal('Anything...');
done();
});
});
});
describe('findById', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope', function(done) {
Product.findById(ids.toolA, function(err, inst) {
should.not.exist(err);
inst.name.should.equal('Tool A');
inst.should.be.instanceof(Tool);
done();
});
});
it('should apply default scope - tool', function(done) {
Tool.findById(ids.toolA, function(err, inst) {
should.not.exist(err);
inst.name.should.equal('Tool A');
done();
});
});
it('should apply default scope (no match)', function(done) {
Widget.findById(ids.toolA, function(err, inst) {
should.not.exist(err);
should.not.exist(inst);
done();
});
});
});
describe('find', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope - order', function(done) {
Product.find(function(err, products) {
should.not.exist(err);
products.should.have.length(5);
products[0].name.should.equal('Tool A');
products[1].name.should.equal('Tool Z');
products[2].name.should.equal('Widget A');
products[3].name.should.equal('Widget B');
products[4].name.should.equal('Widget Z');
products[0].should.be.instanceof(Product);
products[0].should.be.instanceof(Tool);
products[2].should.be.instanceof(Product);
products[2].should.be.instanceof(Widget);
done();
});
});
it('should apply default scope - order override', function(done) {
Product.find({ order: 'name DESC' }, function(err, products) {
should.not.exist(err);
products.should.have.length(5);
products[0].name.should.equal('Widget Z');
products[1].name.should.equal('Widget B');
products[2].name.should.equal('Widget A');
products[3].name.should.equal('Tool Z');
products[4].name.should.equal('Tool A');
done();
});
});
it('should apply default scope - tool', function(done) {
Tool.find(function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].name.should.equal('Tool A');
products[1].name.should.equal('Tool Z');
done();
});
});
it('should apply default scope - where (widget)', function(done) {
Widget.find({ where: { active: true } }, function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].name.should.equal('Widget A');
products[1].name.should.equal('Widget Z');
done();
});
});
it('should apply default scope - order (widget)', function(done) {
Widget.find({ order: 'name DESC' }, function(err, products) {
should.not.exist(err);
products.should.have.length(3);
products[0].name.should.equal('Widget Z');
products[1].name.should.equal('Widget B');
products[2].name.should.equal('Widget A');
done();
});
});
});
describe('exists', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope', function(done) {
Product.exists(ids.widgetA, function(err, exists) {
should.not.exist(err);
exists.should.be.true;
done();
});
});
it('should apply default scope - tool', function(done) {
Tool.exists(ids.toolZ, function(err, exists) {
should.not.exist(err);
exists.should.be.true;
done();
});
});
it('should apply default scope - widget', function(done) {
Widget.exists(ids.widgetA, function(err, exists) {
should.not.exist(err);
exists.should.be.true;
done();
});
});
it('should apply default scope - tool (no match)', function(done) {
Tool.exists(ids.widgetA, function(err, exists) {
should.not.exist(err);
exists.should.be.false;
done();
});
});
it('should apply default scope - widget (no match)', function(done) {
Widget.exists(ids.toolZ, function(err, exists) {
should.not.exist(err);
exists.should.be.false;
done();
});
});
});
describe('count', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope - order', function(done) {
Product.count(function(err, count) {
should.not.exist(err);
count.should.equal(5);
done();
});
});
it('should apply default scope - tool', function(done) {
Tool.count(function(err, count) {
should.not.exist(err);
count.should.equal(2);
done();
});
});
it('should apply default scope - widget', function(done) {
Widget.count(function(err, count) {
should.not.exist(err);
count.should.equal(3);
done();
});
});
it('should apply default scope - where', function(done) {
Widget.count({name: 'Widget Z'}, function(err, count) {
should.not.exist(err);
count.should.equal(1);
done();
});
});
it('should apply default scope - no match', function(done) {
Tool.count({name: 'Widget Z'}, function(err, count) {
should.not.exist(err);
count.should.equal(0);
done();
});
});
});
describe('removeById', function() {
var ids = {};
function isDeleted(id, done) {
Product.exists(id, function(err, exists) {
should.not.exist(err);
exists.should.be.false;
done();
});
};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope', function(done) {
Product.removeById(ids.widgetZ, function(err) {
should.not.exist(err);
isDeleted(ids.widgetZ, done);
});
});
it('should apply default scope - tool', function(done) {
Tool.removeById(ids.toolA, function(err) {
should.not.exist(err);
isDeleted(ids.toolA, done);
});
});
it('should apply default scope - no match', function(done) {
Tool.removeById(ids.widgetA, function(err) {
should.not.exist(err);
Product.exists(ids.widgetA, function(err, exists) {
should.not.exist(err);
exists.should.be.true;
done();
});
});
});
it('should apply default scope - widget', function(done) {
Widget.removeById(ids.widgetA, function(err) {
should.not.exist(err);
isDeleted(ids.widgetA, done);
});
});
it('should apply default scope - verify', function(done) {
Product.find(function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].name.should.equal('Tool Z');
products[1].name.should.equal('Widget B');
done();
});
});
});
describe('update', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope', function(done) {
Widget.update({active: false},{active: true, kind: 'ignored'}, function(err) {
should.not.exist(err);
Widget.find({where: { active: true }}, function(err, products) {
should.not.exist(err);
products.should.have.length(3);
products[0].name.should.equal('Widget A');
products[1].name.should.equal('Widget B');
products[2].name.should.equal('Widget Z');
done();
});
});
});
it('should apply default scope - no match', function(done) {
Tool.update({name: 'Widget A'},{name: 'Ignored'}, function(err) {
should.not.exist(err);
Product.findById(ids.widgetA, function(err, product) {
should.not.exist(err);
product.name.should.equal('Widget A');
done();
});
});
});
it('should have updated within scope', function(done) {
Product.find({where: {active: true}}, function(err, products) {
should.not.exist(err);
products.should.have.length(4);
products[0].name.should.equal('Tool Z');
products[1].name.should.equal('Widget A');
products[2].name.should.equal('Widget B');
products[3].name.should.equal('Widget Z');
done();
});
});
});
describe('remove', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should apply default scope - custom where', function(done) {
Widget.remove({name: 'Widget A'}, function(err) {
should.not.exist(err);
Product.find(function(err, products) {
products.should.have.length(4);
products[0].name.should.equal('Tool A');
products[1].name.should.equal('Tool Z');
products[2].name.should.equal('Widget B');
products[3].name.should.equal('Widget Z');
done();
});
});
});
it('should apply default scope - custom where (no match)', function(done) {
Tool.remove({name: 'Widget Z'}, function(err) {
should.not.exist(err);
Product.find(function(err, products) {
products.should.have.length(4);
products[0].name.should.equal('Tool A');
products[1].name.should.equal('Tool Z');
products[2].name.should.equal('Widget B');
products[3].name.should.equal('Widget Z');
done();
});
});
});
it('should apply default scope - deleteAll', function(done) {
Tool.deleteAll(function(err) {
should.not.exist(err);
Product.find(function(err, products) {
products.should.have.length(2);
products[0].name.should.equal('Widget B');
products[1].name.should.equal('Widget Z');
done();
});
});
});
it('should create a scoped instance - tool', function(done) {
Tool.create({name: 'Tool B'}, function(err, p) {
should.not.exist(err);
Product.find(function(err, products) {
products.should.have.length(3);
products[0].name.should.equal('Tool B');
products[1].name.should.equal('Widget B');
products[2].name.should.equal('Widget Z');
done();
});
});
});
it('should apply default scope - destroyAll', function(done) {
Widget.destroyAll(function(err) {
should.not.exist(err);
Product.find(function(err, products) {
products.should.have.length(1);
products[0].name.should.equal('Tool B');
done();
});
});
});
});
describe('scopes', function() {
var ids = {};
before(function (done) {
db.automigrate(setupProducts.bind(null, ids, done));
});
it('should merge with default scope', function(done) {
Product.active(function(err, products) {
should.not.exist(err);
products.should.have.length(3);
products[0].name.should.equal('Tool Z');
products[1].name.should.equal('Widget A');
products[2].name.should.equal('Widget Z');
done();
});
});
it('should merge with default scope - tool', function(done) {
Tool.active(function(err, products) {
should.not.exist(err);
products.should.have.length(1);
products[0].name.should.equal('Tool Z');
done();
});
});
it('should merge with default scope - widget', function(done) {
Widget.active(function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].name.should.equal('Widget A');
products[1].name.should.equal('Widget Z');
done();
});
});
});
describe('scope function', function() {
before(function(done) {
db.automigrate(done);
});
it('should create a scoped instance - widget', function(done) {
Widget.create({name: 'Product', kind:'ignored'}, function(err, p) {
p.name.should.equal('Product');
p.kind.should.equal('Widget');
done();
});
});
it('should create a scoped instance - thing', function(done) {
Thing.create({name: 'Product', kind:'ignored'}, function(err, p) {
p.name.should.equal('Product');
p.kind.should.equal('Thing');
done();
});
});
it('should find a scoped instance - widget', function(done) {
Widget.findOne({where: {name: 'Product'}}, function(err, p) {
p.name.should.equal('Product');
p.kind.should.equal('Widget');
done();
});
});
it('should find a scoped instance - thing', function(done) {
Thing.findOne({where: {name: 'Product'}}, function(err, p) {
p.name.should.equal('Product');
p.kind.should.equal('Thing');
done();
});
});
it('should find a scoped instance - thing', function(done) {
Product.find({where: {name: 'Product'}}, function(err, products) {
products.should.have.length(2);
products[0].name.should.equal('Product');
products[1].name.should.equal('Product');
var kinds = products.map(function(p) { return p.kind; })
kinds.sort();
kinds.should.eql(['Thing', 'Widget']);
done();
});
});
});
describe('relations', function() {
var ids = {};
before(function (done) {
db.automigrate(done);
});
before(function (done) {
Category.create({name: 'Category A'}, function(err, cat) {
ids.categoryA = cat.id;
async.series([
function(next) {
cat.widgets.create({name: 'Widget B', kind: 'ignored'}, next);
},
function(next) {
cat.widgets.create({name: 'Widget A'}, next);
},
function(next) {
cat.tools.create({name: 'Tool A'}, next);
},
function(next) {
cat.things.create({name: 'Thing A'}, next);
}
], done);
});
});
it('should apply default scope - products', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
should.not.exist(err);
cat.products(function(err, products) {
should.not.exist(err);
products.should.have.length(4);
products[0].name.should.equal('Thing A');
products[1].name.should.equal('Tool A');
products[2].name.should.equal('Widget A');
products[3].name.should.equal('Widget B');
products[0].should.be.instanceof(Product);
products[0].should.be.instanceof(Thing);
products[1].should.be.instanceof(Product);
products[1].should.be.instanceof(Tool);
products[2].should.be.instanceof(Product);
products[2].should.be.instanceof(Widget);
done();
});
});
});
it('should apply default scope - widgets', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
should.not.exist(err);
cat.widgets(function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].should.be.instanceof(Widget);
products[0].name.should.equal('Widget A');
products[1].name.should.equal('Widget B');
products[0].category(function(err, inst) {
inst.name.should.equal('Category A');
done();
});
});
});
});
it('should apply default scope - tools', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
should.not.exist(err);
cat.tools(function(err, products) {
should.not.exist(err);
products.should.have.length(1);
products[0].should.be.instanceof(Tool);
products[0].name.should.equal('Tool A');
products[0].category(function(err, inst) {
inst.name.should.equal('Category A');
done();
});
});
});
});
it('should apply default scope - things', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
should.not.exist(err);
cat.things(function(err, products) {
should.not.exist(err);
products.should.have.length(1);
products[0].should.be.instanceof(Thing);
products[0].name.should.equal('Thing A');
products[0].category(function(err, inst) {
inst.name.should.equal('Category A');
done();
});
});
});
});
it('should create related item with default scope', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
cat.tools.create({name: 'Tool B'}, done);
});
});
it('should use relation scope order', function(done) {
Category.findById(ids.categoryA, function(err, cat) {
should.not.exist(err);
cat.tools(function(err, products) {
should.not.exist(err);
products.should.have.length(2);
products[0].name.should.equal('Tool B');
products[1].name.should.equal('Tool A');
done();
});
});
});
});
});

View File

@ -79,6 +79,17 @@ describe('include', function () {
done(); done();
}); });
}); });
it('should fetch Passport - Owner - Posts - alternate syntax', function (done) {
Passport.find({include: {owner: {relation: 'posts'}}}, function (err, passports) {
should.not.exist(err);
should.exist(passports);
passports.length.should.be.ok;
var posts = passports[0].owner().posts();
posts.should.have.length(3);
done();
});
});
it('should fetch Passports - User - Posts - User', function (done) { it('should fetch Passports - User - Posts - User', function (done) {
Passport.find({ Passport.find({
@ -97,6 +108,7 @@ describe('include', function () {
user.id.should.equal(p.ownerId); user.id.should.equal(p.ownerId);
user.__cachedRelations.should.have.property('posts'); user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.posts.forEach(function (pp) { user.__cachedRelations.posts.forEach(function (pp) {
pp.should.have.property('id');
pp.userId.should.equal(user.id); pp.userId.should.equal(user.id);
pp.should.have.property('author'); pp.should.have.property('author');
pp.__cachedRelations.should.have.property('author'); pp.__cachedRelations.should.have.property('author');
@ -108,6 +120,92 @@ describe('include', function () {
done(); done();
}); });
}); });
it('should fetch Passports with include scope on Posts', function (done) {
Passport.find({
include: {owner: {relation: 'posts', scope:{
fields: ['title'], include: ['author'],
order: 'title DESC'
}}}
}, function (err, passports) {
should.not.exist(err);
should.exist(passports);
passports.length.should.equal(3);
var passport = passports[0];
passport.number.should.equal('1');
passport.owner().name.should.equal('User A');
var owner = passport.owner().toObject();
var posts = passport.owner().posts();
posts.should.be.an.array;
posts.should.have.length(3);
posts[0].title.should.equal('Post C');
posts[0].should.not.have.property('id'); // omitted
posts[0].author().should.be.instanceOf(User);
posts[0].author().name.should.equal('User A');
posts[1].title.should.equal('Post B');
posts[1].author().name.should.equal('User A');
posts[2].title.should.equal('Post A');
posts[2].author().name.should.equal('User A');
done();
});
});
it('should fetch Users with include scope on Posts', function (done) {
User.find({
include: {relation: 'posts', scope:{
order: 'title DESC'
}}
}, function (err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.equal(5);
users[0].name.should.equal('User A');
users[1].name.should.equal('User B');
var posts = users[0].posts();
posts.should.be.an.array;
posts.should.have.length(3);
posts[0].title.should.equal('Post C');
posts[1].title.should.equal('Post B');
posts[2].title.should.equal('Post A');
var posts = users[1].posts();
posts.should.be.an.array;
posts.should.have.length(1);
posts[0].title.should.equal('Post D');
done();
});
});
it('should fetch Users with include scope on Passports', function (done) {
User.find({
include: {relation: 'passports', scope:{
where: { number: '2' }
}}
}, function (err, users) {
should.not.exist(err);
should.exist(users);
users.length.should.equal(5);
users[0].name.should.equal('User A');
users[0].passports().should.be.empty;
users[1].name.should.equal('User B');
var passports = users[1].passports();
passports[0].number.should.equal('2');
done();
});
});
it('should fetch User - Posts AND Passports', function (done) { it('should fetch User - Posts AND Passports', function (done) {
User.find({include: ['posts', 'passports']}, function (err, users) { User.find({include: ['posts', 'passports']}, function (err, users) {

View File

@ -1,8 +1,29 @@
var assert = require('assert'); var assert = require('assert');
var ModelBuilder = require('../lib/model-builder').ModelBuilder; var ModelBuilder = require('..').ModelBuilder;
var DataSource = require('../').DataSource;
var introspectType = require('../lib/introspection')(ModelBuilder); var introspectType = require('../lib/introspection')(ModelBuilder);
var traverse = require('traverse'); var traverse = require('traverse');
var json = {
name: 'Joe',
age: 30,
birthday: new Date(),
vip: true,
address: {
street: '1 Main St',
city: 'San Jose',
state: 'CA',
zipcode: '95131',
country: 'US'
},
friends: ['John', 'Mary'],
emails: [
{label: 'work', id: 'x@sample.com'},
{label: 'home', id: 'x@home.com'}
],
tags: []
};
describe('Introspection of model definitions from JSON', function () { describe('Introspection of model definitions from JSON', function () {
it('should handle simple types', function () { it('should handle simple types', function () {
@ -61,27 +82,6 @@ describe('Introspection of model definitions from JSON', function () {
}); });
it('should build a model from the introspected schema', function (done) { it('should build a model from the introspected schema', function (done) {
var json = {
name: 'Joe',
age: 30,
birthday: new Date(),
vip: true,
address: {
street: '1 Main St',
city: 'San Jose',
state: 'CA',
zipcode: '95131',
country: 'US'
},
friends: ['John', 'Mary'],
emails: [
{label: 'work', id: 'x@sample.com'},
{label: 'home', id: 'x@home.com'}
],
tags: []
};
var copy = traverse(json).clone(); var copy = traverse(json).clone();
var schema = introspectType(json); var schema = introspectType(json);
@ -97,5 +97,33 @@ describe('Introspection of model definitions from JSON', function () {
assert.deepEqual(obj, copy); assert.deepEqual(obj, copy);
done(); done();
}); });
it('should build a model using buildModelFromInstance', function (done) {
var copy = traverse(json).clone();
var builder = new ModelBuilder();
var Model = builder.buildModelFromInstance('MyModel', copy, {idInjection: false});
var obj = new Model(json);
obj = obj.toObject();
assert.deepEqual(obj, copy);
done();
});
it('should build a model using DataSource.buildModelFromInstance', function (done) {
var copy = traverse(json).clone();
var builder = new DataSource('memory');
var Model = builder.buildModelFromInstance('MyModel', copy,
{idInjection: false});
assert.equal(Model.dataSource, builder);
var obj = new Model(json);
obj = obj.toObject();
assert.deepEqual(obj, copy);
done();
});
}); });

View File

@ -639,7 +639,7 @@ describe('Load models with base', function () {
assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod); assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod);
assert.equal(Customer.base, User); assert.equal(Customer.base, User);
assert.equal(Customer.base, Customer.super_); assert.equal(Customer.base, Customer.super_);
try { try {
var Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'}); var Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'});
} catch (e) { } catch (e) {
@ -1418,28 +1418,31 @@ describe('DataAccessObject', function () {
}); });
describe('Load models from json', function () { describe('Load models from json', function () {
it('should be able to define models from json', function () { var path = require('path'),
var path = require('path'), fs = require('fs');
fs = require('fs');
/**
* Load LDL schemas from a json doc
* @param schemaFile The dataSource json file
* @returns A map of schemas keyed by name
*/
function loadSchemasSync(schemaFile, dataSource) {
// Set up the data source
if (!dataSource) {
dataSource = new DataSource('memory');
}
// Read the dataSource JSON file
var schemas = JSON.parse(fs.readFileSync(schemaFile));
return dataSource.modelBuilder.buildModels(schemas);
/**
* Load LDL schemas from a json doc
* @param schemaFile The dataSource json file
* @returns A map of schemas keyed by name
*/
function loadSchemasSync(schemaFile, dataSource) {
var modelBuilder, createModel;
// Set up the data source
if (!dataSource) {
modelBuilder = new ModelBuilder();
} else {
modelBuilder = dataSource.modelBuilder;
createModel = dataSource.createModel.bind(dataSource);
} }
// Read the dataSource JSON file
var schemas = JSON.parse(fs.readFileSync(schemaFile));
return modelBuilder.buildModels(schemas, createModel);
}
it('should be able to define models from json', function () {
var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json')); var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json'));
models.should.have.property('AnonymousModel_0'); models.should.have.property('AnonymousModel_0');
@ -1459,6 +1462,16 @@ describe('Load models from json', function () {
} }
}); });
it('should be able to define models from json using dataSource', function() {
var ds = new DataSource('memory');
var models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json'), ds);
models.should.have.property('Address');
models.should.have.property('Account');
models.should.have.property('Customer');
assert.equal(models.Address.dataSource, ds);
});
it('should allow customization of default model base class', function () { it('should allow customization of default model base class', function () {
var modelBuilder = new ModelBuilder(); var modelBuilder = new ModelBuilder();

View File

@ -269,6 +269,48 @@ describe('Memory connector', function () {
} }
}); });
it('should use collection setting', function (done) {
var ds = new DataSource({
connector: 'memory'
});
var Product = ds.createModel('Product', {
name: String
});
var Tool = ds.createModel('Tool', {
name: String
}, {memory: {collection: 'Product'}});
var Widget = ds.createModel('Widget', {
name: String
}, {memory: {collection: 'Product'}});
ds.connector.getCollection('Tool').should.equal('Product');
ds.connector.getCollection('Widget').should.equal('Product');
async.series([
function(next) {
Tool.create({ name: 'Tool A' }, next);
},
function(next) {
Tool.create({ name: 'Tool B' }, next);
},
function(next) {
Widget.create({ name: 'Widget A' }, next);
}
], function(err) {
Product.find(function(err, products) {
should.not.exist(err);
products.should.have.length(3);
products[0].toObject().should.eql({ name: 'Tool A', id: 1 });
products[1].toObject().should.eql({ name: 'Tool B', id: 2 });
products[2].toObject().should.eql({ name: 'Widget A', id: 3 });
done();
});
});
});
}); });

View File

@ -250,6 +250,26 @@ describe('ModelDefinition class', function () {
assert(anotherChild.prototype instanceof baseChild); assert(anotherChild.prototype instanceof baseChild);
}); });
it('should ignore inherited options.base', function() {
var memory = new DataSource({connector: Memory});
var modelBuilder = memory.modelBuilder;
var base = modelBuilder.define('base');
var child = base.extend('child', {}, { base: 'base' });
var grandChild = child.extend('grand-child');
assert.equal('child', grandChild.base.modelName);
assert(grandChild.prototype instanceof child);
});
it('should ignore inherited options.super', function() {
var memory = new DataSource({connector: Memory});
var modelBuilder = memory.modelBuilder;
var base = modelBuilder.define('base');
var child = base.extend('child', {}, { super: 'base' });
var grandChild = child.extend('grand-child');
assert.equal('child', grandChild.base.modelName);
assert(grandChild.prototype instanceof child);
});
it('should not serialize hidden properties into JSON', function () { it('should not serialize hidden properties into JSON', function () {
var memory = new DataSource({connector: Memory}); var memory = new DataSource({connector: Memory});
var modelBuilder = memory.modelBuilder; var modelBuilder = memory.modelBuilder;

View File

@ -335,7 +335,7 @@ describe('relations', function () {
physician.patients.findById(id, function (err, ch) { physician.patients.findById(id, function (err, ch) {
should.not.exist(err); should.not.exist(err);
should.exist(ch); should.exist(ch);
ch.id.should.equal(id); ch.id.should.eql(id);
done(); done();
}); });
} }
@ -387,7 +387,7 @@ describe('relations', function () {
physician.patients.findById(id, function (err, ch) { physician.patients.findById(id, function (err, ch) {
should.not.exist(err); should.not.exist(err);
should.exist(ch); should.exist(ch);
ch.id.should.equal(id); ch.id.should.eql(id);
ch.name.should.equal('aa'); ch.name.should.equal('aa');
done(); done();
}); });
@ -1624,15 +1624,15 @@ describe('relations', function () {
}); });
describe('hasAndBelongsToMany', function () { describe('hasAndBelongsToMany', function () {
var Article, Tag, ArticleTag; var Article, TagName, ArticleTag;
it('can be declared', function (done) { it('can be declared', function (done) {
Article = db.define('Article', {title: String}); Article = db.define('Article', {title: String});
Tag = db.define('Tag', {name: String}); TagName = db.define('TagName', {name: String});
Article.hasAndBelongsToMany('tags'); Article.hasAndBelongsToMany('tagNames');
ArticleTag = db.models.ArticleTag; ArticleTag = db.models.ArticleTagName;
db.automigrate(function () { db.automigrate(function () {
Article.destroyAll(function () { Article.destroyAll(function () {
Tag.destroyAll(function () { TagName.destroyAll(function () {
ArticleTag.destroyAll(done) ArticleTag.destroyAll(done)
}); });
}); });
@ -1641,11 +1641,11 @@ describe('relations', function () {
it('should allow to create instances on scope', function (done) { it('should allow to create instances on scope', function (done) {
Article.create(function (e, article) { Article.create(function (e, article) {
article.tags.create({name: 'popular'}, function (e, t) { article.tagNames.create({name: 'popular'}, function (e, t) {
t.should.be.an.instanceOf(Tag); t.should.be.an.instanceOf(TagName);
ArticleTag.findOne(function (e, at) { ArticleTag.findOne(function (e, at) {
should.exist(at); should.exist(at);
at.tagId.toString().should.equal(t.id.toString()); at.tagNameId.toString().should.equal(t.id.toString());
at.articleId.toString().should.equal(article.id.toString()); at.articleId.toString().should.equal(article.id.toString());
done(); done();
}); });
@ -1655,11 +1655,11 @@ describe('relations', function () {
it('should allow to fetch scoped instances', function (done) { it('should allow to fetch scoped instances', function (done) {
Article.findOne(function (e, article) { Article.findOne(function (e, article) {
article.tags(function (e, tags) { article.tagNames(function (e, tags) {
should.not.exist(e); should.not.exist(e);
should.exist(tags); should.exist(tags);
article.tags().should.eql(tags); article.tagNames().should.eql(tags);
done(); done();
}); });
@ -1668,12 +1668,12 @@ describe('relations', function () {
it('should allow to add connection with instance', function (done) { it('should allow to add connection with instance', function (done) {
Article.findOne(function (e, article) { Article.findOne(function (e, article) {
Tag.create({name: 'awesome'}, function (e, tag) { TagName.create({name: 'awesome'}, function (e, tag) {
article.tags.add(tag, function (e, at) { article.tagNames.add(tag, function (e, at) {
should.not.exist(e); should.not.exist(e);
should.exist(at); should.exist(at);
at.should.be.an.instanceOf(ArticleTag); at.should.be.an.instanceOf(ArticleTag);
at.tagId.should.equal(tag.id); at.tagNameId.should.equal(tag.id);
at.articleId.should.equal(article.id); at.articleId.should.equal(article.id);
done(); done();
}); });
@ -1683,12 +1683,12 @@ describe('relations', function () {
it('should allow to remove connection with instance', function (done) { it('should allow to remove connection with instance', function (done) {
Article.findOne(function (e, article) { Article.findOne(function (e, article) {
article.tags(function (e, tags) { article.tagNames(function (e, tags) {
var len = tags.length; var len = tags.length;
tags.should.not.be.empty; tags.should.not.be.empty;
article.tags.remove(tags[0], function (e) { article.tagNames.remove(tags[0], function (e) {
should.not.exist(e); should.not.exist(e);
article.tags(true, function (e, tags) { article.tagNames(true, function (e, tags) {
tags.should.have.lengthOf(len - 1); tags.should.have.lengthOf(len - 1);
done(); done();
}); });
@ -1698,7 +1698,7 @@ describe('relations', function () {
}); });
it('should set targetClass on scope property', function() { it('should set targetClass on scope property', function() {
should.equal(Article.prototype.tags._targetClass, 'Tag'); should.equal(Article.prototype.tagNames._targetClass, 'TagName');
}); });
}); });