Merge branch 'release/2.10.0' into production
This commit is contained in:
commit
9807f2dbb7
|
@ -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
|
|
@ -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) {
|
||||
var self = this;
|
||||
var hasLocalStorage = typeof window !== 'undefined' && window.localStorage;
|
||||
|
@ -161,36 +186,31 @@ Memory.prototype.saveToFile = function (result, callback) {
|
|||
Memory.prototype.define = function defineModel(definition) {
|
||||
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
|
||||
var m = definition.model.modelName;
|
||||
if(!this.cache[m]) {
|
||||
this.cache[m] = {};
|
||||
this.ids[m] = 1;
|
||||
}
|
||||
if(!this.collection(m)) this.initCollection(m);
|
||||
};
|
||||
|
||||
Memory.prototype.create = function create(model, data, 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.ids[model];
|
||||
if (currentId === undefined) {
|
||||
// First time
|
||||
this.ids[model] = 1;
|
||||
currentId = 1;
|
||||
var currentId = this.collectionSeq(model);
|
||||
if (currentId === undefined) { // First time
|
||||
currentId = this.collectionSeq(model, 1);
|
||||
}
|
||||
var id = this.getIdValue(model, data) || currentId;
|
||||
if (id > currentId) {
|
||||
// If the id is passed in and the value is greater than the current id
|
||||
currentId = id;
|
||||
}
|
||||
this.ids[model] = Number(currentId) + 1;
|
||||
this.collectionSeq(model, Number(currentId) + 1);
|
||||
|
||||
var props = this._models[model].properties;
|
||||
var idName = this.idName(model);
|
||||
id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
|
||||
this.setIdValue(model, data, id);
|
||||
if(!this.cache[model]) {
|
||||
this.cache[model] = {};
|
||||
if(!this.collection(model)) {
|
||||
this.collection(model, {});
|
||||
}
|
||||
this.cache[model][id] = serialize(data);
|
||||
this.collection(model)[id] = serialize(data);
|
||||
this.saveToFile(id, callback);
|
||||
};
|
||||
|
||||
|
@ -210,30 +230,30 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
|
|||
|
||||
Memory.prototype.save = function save(model, data, callback) {
|
||||
var id = this.getIdValue(model, data);
|
||||
var cachedModels = this.cache[model];
|
||||
var modelData = cachedModels && this.cache[model][id];
|
||||
var cachedModels = this.collection(model);
|
||||
var modelData = cachedModels && this.collection(model)[id];
|
||||
modelData = modelData && deserialize(modelData);
|
||||
if (modelData) {
|
||||
data = merge(modelData, data);
|
||||
}
|
||||
this.cache[model][id] = serialize(data);
|
||||
this.collection(model)[id] = serialize(data);
|
||||
this.saveToFile(data, callback);
|
||||
};
|
||||
|
||||
Memory.prototype.exists = function exists(model, id, callback) {
|
||||
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));
|
||||
};
|
||||
|
||||
Memory.prototype.find = function find(model, id, callback) {
|
||||
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));
|
||||
};
|
||||
|
||||
Memory.prototype.destroy = function destroy(model, id, callback) {
|
||||
delete this.cache[model][id];
|
||||
delete this.collection(model)[id];
|
||||
this.saveToFile(null, callback);
|
||||
};
|
||||
|
||||
|
@ -266,8 +286,8 @@ Memory.prototype.fromDb = function (model, data) {
|
|||
|
||||
Memory.prototype.all = function all(model, filter, callback) {
|
||||
var self = this;
|
||||
var nodes = Object.keys(this.cache[model]).map(function (key) {
|
||||
return this.fromDb(model, this.cache[model][key]);
|
||||
var nodes = Object.keys(this.collection(model)).map(function (key) {
|
||||
return this.fromDb(model, this.collection(model)[key]);
|
||||
}.bind(this));
|
||||
|
||||
if (filter) {
|
||||
|
@ -505,24 +525,23 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
|
|||
callback = where;
|
||||
where = undefined;
|
||||
}
|
||||
var cache = this.cache[model];
|
||||
var cache = this.collection(model);
|
||||
var filter = null;
|
||||
if (where) {
|
||||
filter = applyFilter({where: where});
|
||||
}
|
||||
Object.keys(cache).forEach(function (id) {
|
||||
if (!filter || filter(this.fromDb(model, cache[id]))) {
|
||||
delete cache[id];
|
||||
}
|
||||
}.bind(this));
|
||||
if (!where) {
|
||||
this.cache[model] = {};
|
||||
} else {
|
||||
this.collection(model, {});
|
||||
}
|
||||
this.saveToFile(null, callback);
|
||||
};
|
||||
|
||||
Memory.prototype.count = function count(model, callback, where) {
|
||||
var cache = this.cache[model];
|
||||
var cache = this.collection(model);
|
||||
var data = Object.keys(cache);
|
||||
if (where) {
|
||||
var filter = {where: where};
|
||||
|
@ -539,7 +558,7 @@ Memory.prototype.count = function count(model, callback, where) {
|
|||
Memory.prototype.update =
|
||||
Memory.prototype.updateAll = function updateAll(model, where, data, cb) {
|
||||
var self = this;
|
||||
var cache = this.cache[model];
|
||||
var cache = this.collection(model);
|
||||
var filter = null;
|
||||
where = where || {};
|
||||
filter = applyFilter({where: where});
|
||||
|
@ -571,8 +590,8 @@ Memory.prototype.updateAttributes = function updateAttributes(model, id, data, c
|
|||
|
||||
this.setIdValue(model, data, id);
|
||||
|
||||
var cachedModels = this.cache[model];
|
||||
var modelData = cachedModels && this.cache[model][id];
|
||||
var cachedModels = this.collection(model);
|
||||
var modelData = cachedModels && this.collection(model)[id];
|
||||
|
||||
if (modelData) {
|
||||
this.save(model, data, cb);
|
||||
|
@ -594,6 +613,16 @@ Memory.prototype.buildNearFilter = function (filter) {
|
|||
// 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) {
|
||||
if (!base) {
|
||||
return update;
|
||||
|
|
138
lib/dao.js
138
lib/dao.js
|
@ -13,11 +13,12 @@ var Relation = require('./relations.js');
|
|||
var Inclusion = require('./include.js');
|
||||
var List = require('./list.js');
|
||||
var geo = require('./geo');
|
||||
var mergeQuery = require('./scope.js').mergeQuery;
|
||||
var Memory = require('./connectors/memory').Memory;
|
||||
var utils = require('./utils');
|
||||
var fieldsToArray = utils.fieldsToArray;
|
||||
var removeUndefined = utils.removeUndefined;
|
||||
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
|
||||
var mergeQuery = utils.mergeQuery;
|
||||
var util = require('util');
|
||||
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) {
|
||||
if (!(this.getDataSource().isRelational && this.getDataSource().isRelational())) {
|
||||
return data;
|
||||
|
@ -69,6 +78,40 @@ DataAccessObject._forDB = function (data) {
|
|||
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.
|
||||
* Example:
|
||||
|
@ -89,7 +132,7 @@ DataAccessObject.create = function (data, callback) {
|
|||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||
|
||||
var Model = this;
|
||||
var modelName = Model.modelName;
|
||||
var self = this;
|
||||
|
||||
if (typeof data === 'function') {
|
||||
callback = data;
|
||||
|
@ -116,6 +159,7 @@ DataAccessObject.create = function (data, callback) {
|
|||
|
||||
for (var i = 0; i < data.length; i += 1) {
|
||||
(function (d, i) {
|
||||
Model = self.lookupModel(d); // data-specific
|
||||
instances.push(Model.create(d, function (err, inst) {
|
||||
if (err) {
|
||||
errors[i] = err;
|
||||
|
@ -131,11 +175,16 @@ DataAccessObject.create = function (data, callback) {
|
|||
function modelCreated() {
|
||||
if (--wait === 0) {
|
||||
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 idValue = getIdValue(this, data);
|
||||
|
||||
|
@ -145,6 +194,13 @@ DataAccessObject.create = function (data, callback) {
|
|||
} else {
|
||||
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);
|
||||
|
||||
// validation required
|
||||
|
@ -161,6 +217,7 @@ DataAccessObject.create = function (data, callback) {
|
|||
obj.trigger('save', function (saveDone) {
|
||||
|
||||
var _idName = idName(Model);
|
||||
var modelName = Model.modelName;
|
||||
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
|
||||
if (id) {
|
||||
obj.__data[_idName] = id;
|
||||
|
@ -208,7 +265,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
if (stillConnecting(this.getDataSource(), this, arguments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var Model = this;
|
||||
if (!getIdValue(this, data)) {
|
||||
return this.create(data, callback);
|
||||
|
@ -220,7 +277,9 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
inst = new Model(data);
|
||||
}
|
||||
update = inst.toObject(false);
|
||||
this.applyProperties(update, inst);
|
||||
update = removeUndefined(update);
|
||||
Model = this.lookupModel(update);
|
||||
this.getDataSource().connector.updateOrCreate(Model.modelName, update, function (err, data) {
|
||||
var obj;
|
||||
if (data && !(data instanceof Model)) {
|
||||
|
@ -242,6 +301,7 @@ DataAccessObject.updateOrCreate = DataAccessObject.upsert = function upsert(data
|
|||
if (inst) {
|
||||
inst.updateAttributes(data, callback);
|
||||
} else {
|
||||
Model = self.lookupModel(data);
|
||||
var obj = new Model(data);
|
||||
obj.save(data, callback);
|
||||
}
|
||||
|
@ -290,7 +350,9 @@ DataAccessObject.exists = function exists(id, cb) {
|
|||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||
|
||||
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 {
|
||||
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) {
|
||||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||
|
||||
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));
|
||||
this.findOne(byIdQuery(this, id), cb);
|
||||
};
|
||||
|
||||
DataAccessObject.findByIds = function(ids, cond, cb) {
|
||||
|
@ -692,6 +744,8 @@ DataAccessObject.find = function find(query, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
this.applyScope(query);
|
||||
|
||||
var near = query && geo.nearFilter(query.where);
|
||||
var supportsGeo = !!this.getDataSource().connector.buildNearFilter;
|
||||
|
||||
|
@ -702,6 +756,7 @@ DataAccessObject.find = function find(query, cb) {
|
|||
} else if (query.where) {
|
||||
// do in memory query
|
||||
// using all documents
|
||||
// TODO [fabien] use default scope here?
|
||||
this.getDataSource().connector.all(this.modelName, {}, function (err, data) {
|
||||
var memory = new Memory();
|
||||
var modelName = self.modelName;
|
||||
|
@ -735,7 +790,8 @@ DataAccessObject.find = function find(query, cb) {
|
|||
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
|
||||
if (data && data.forEach) {
|
||||
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.collect) {
|
||||
|
@ -747,19 +803,19 @@ DataAccessObject.find = function find(query, cb) {
|
|||
// This handles the case to return parent items including the related
|
||||
// models. For example, Article.find({include: 'tags'}, ...);
|
||||
// Try to normalize the include
|
||||
var includes = query.include || [];
|
||||
if (typeof includes === 'string') {
|
||||
includes = [includes];
|
||||
} else if (!Array.isArray(includes) && typeof includes === 'object') {
|
||||
includes = Object.keys(includes);
|
||||
}
|
||||
var includes = Inclusion.normalizeInclude(query.include || []);
|
||||
includes.forEach(function (inc) {
|
||||
var relationName = inc;
|
||||
if (utils.isPlainObject(inc)) {
|
||||
relationName = Object.keys(inc)[0];
|
||||
}
|
||||
|
||||
// Promote the included model as a direct property
|
||||
var data = obj.__cachedRelations[inc];
|
||||
var data = obj.__cachedRelations[relationName];
|
||||
if(Array.isArray(data)) {
|
||||
data = new List(data, null, obj);
|
||||
}
|
||||
obj.__data[inc] = data;
|
||||
if (data) obj.__data[relationName] = data;
|
||||
});
|
||||
delete obj.__data.__cachedRelations;
|
||||
}
|
||||
|
@ -824,7 +880,12 @@ DataAccessObject.remove = DataAccessObject.deleteAll = DataAccessObject.destroyA
|
|||
cb = where;
|
||||
where = undefined;
|
||||
}
|
||||
if (!where) {
|
||||
|
||||
var query = { where: where };
|
||||
this.applyScope(query);
|
||||
where = query.where;
|
||||
|
||||
if (!where || (typeof where === 'object' && Object.keys(where).length === 0)) {
|
||||
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
|
||||
cb && cb(err, data);
|
||||
if(!err) Model.emit('deletedAll');
|
||||
|
@ -860,12 +921,12 @@ DataAccessObject.removeById = DataAccessObject.destroyById = DataAccessObject.de
|
|||
if (stillConnecting(this.getDataSource(), this, arguments)) return;
|
||||
var Model = this;
|
||||
|
||||
this.getDataSource().connector.destroy(this.modelName, id, function (err) {
|
||||
this.remove(byIdQuery(this, id).where, function(err) {
|
||||
if ('function' === typeof cb) {
|
||||
cb(err);
|
||||
}
|
||||
if(!err) Model.emit('deleted', id);
|
||||
}.bind(this));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -888,6 +949,11 @@ DataAccessObject.count = function (where, cb) {
|
|||
cb = where;
|
||||
where = null;
|
||||
}
|
||||
|
||||
var query = { where: where };
|
||||
this.applyScope(query);
|
||||
where = query.where;
|
||||
|
||||
try {
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
|
@ -896,6 +962,7 @@ DataAccessObject.count = function (where, cb) {
|
|||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
this.getDataSource().connector.count(this.modelName, cb, where);
|
||||
};
|
||||
|
||||
|
@ -931,8 +998,12 @@ DataAccessObject.prototype.save = function (options, callback) {
|
|||
var data = inst.toObject(true);
|
||||
var modelName = Model.modelName;
|
||||
|
||||
Model.applyProperties(data, this);
|
||||
|
||||
if (this.isNewRecord()) {
|
||||
return Model.create(this, callback);
|
||||
} else {
|
||||
inst.setAttributes(data);
|
||||
}
|
||||
|
||||
// validate first
|
||||
|
@ -1017,6 +1088,12 @@ DataAccessObject.updateAll = function (where, data, cb) {
|
|||
assert(typeof data === 'object', 'The data argument should be an object');
|
||||
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 {
|
||||
where = removeUndefined(where);
|
||||
where = this._coerce(where);
|
||||
|
@ -1025,6 +1102,7 @@ DataAccessObject.updateAll = function (where, data, cb) {
|
|||
cb && cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
var connector = this.getDataSource().connector;
|
||||
connector.update(this.modelName, where, data, cb);
|
||||
};
|
||||
|
@ -1075,7 +1153,7 @@ DataAccessObject.prototype.remove =
|
|||
* @param {Mixed} value Value of property
|
||||
*/
|
||||
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) {
|
||||
if (typeof data !== 'object') return;
|
||||
|
||||
this.constructor.applyProperties(data, this);
|
||||
|
||||
var Model = this.constructor;
|
||||
var inst = this;
|
||||
|
||||
|
|
|
@ -1427,6 +1427,7 @@ DataSource.prototype.discoverSchemasSync = function (modelName, options) {
|
|||
*/
|
||||
DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
this.discoverSchemas(modelName, options, function (err, schemas) {
|
||||
if (err) {
|
||||
cb && cb(err, schemas);
|
||||
|
@ -1436,14 +1437,16 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb)
|
|||
var schemaList = [];
|
||||
for (var s in schemas) {
|
||||
var schema = schemas[s];
|
||||
if (options.base) {
|
||||
schema.options = schema.options || {};
|
||||
schema.options.base = options.base;
|
||||
}
|
||||
schemaList.push(schema);
|
||||
}
|
||||
|
||||
var models = self.modelBuilder.buildModels(schemaList);
|
||||
// Now attach the models to the data source
|
||||
for (var m in models) {
|
||||
models[m].attachTo(self);
|
||||
}
|
||||
var models = self.modelBuilder.buildModels(schemaList,
|
||||
self.createModel.bind(self));
|
||||
|
||||
cb && cb(err, models);
|
||||
});
|
||||
};
|
||||
|
@ -1462,18 +1465,41 @@ DataSource.prototype.discoverAndBuildModels = function (modelName, options, cb)
|
|||
* @param {Object} [options] The options
|
||||
*/
|
||||
DataSource.prototype.discoverAndBuildModelsSync = function (modelName, options) {
|
||||
options = options || {};
|
||||
var schemas = this.discoverSchemasSync(modelName, options);
|
||||
|
||||
var schemaList = [];
|
||||
for (var s in schemas) {
|
||||
var schema = schemas[s];
|
||||
if (options.base) {
|
||||
schema.options = schema.options || {};
|
||||
schema.options.base = options.base;
|
||||
}
|
||||
schemaList.push(schema);
|
||||
}
|
||||
|
||||
var models = this.modelBuilder.buildModels(schemaList);
|
||||
var models = this.modelBuilder.buildModels(schemaList,
|
||||
this.createModel.bind(this));
|
||||
|
||||
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
|
||||
* This method applies only to SQL connectors.
|
||||
|
|
115
lib/include.js
115
lib/include.js
|
@ -3,6 +3,53 @@ var utils = require('./utils');
|
|||
var isPlainObject = utils.isPlainObject;
|
||||
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
|
||||
*/
|
||||
|
@ -17,6 +64,13 @@ module.exports = Inclusion;
|
|||
function Inclusion() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize includes - used in DataAccessObject
|
||||
*
|
||||
*/
|
||||
|
||||
Inclusion.normalizeInclude = normalizeInclude;
|
||||
|
||||
/**
|
||||
* Enables you to load relations of several objects and optimize numbers of requests.
|
||||
*
|
||||
|
@ -59,42 +113,26 @@ Inclusion.include = function (objects, include, cb) {
|
|||
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) {
|
||||
var relations = self.relations;
|
||||
|
||||
var relationName, subInclude;
|
||||
var relationName;
|
||||
var subInclude = null, scope = null;
|
||||
|
||||
if (isPlainObject(include)) {
|
||||
relationName = Object.keys(include)[0];
|
||||
if (include[relationName] instanceof IncludeScope) {
|
||||
scope = include[relationName];
|
||||
subInclude = scope.include();
|
||||
} else {
|
||||
subInclude = include[relationName];
|
||||
}
|
||||
} else {
|
||||
relationName = include;
|
||||
subInclude = null;
|
||||
}
|
||||
var relation = relations[relationName];
|
||||
|
||||
var relation = relations[relationName];
|
||||
if (!relation) {
|
||||
cb(new Error('Relation "' + relationName + '" is not defined for '
|
||||
+ self.modelName + ' model'));
|
||||
|
@ -126,10 +164,35 @@ Inclusion.include = function (objects, include, cb) {
|
|||
|
||||
var inst = (obj instanceof self) ? obj : new self(obj);
|
||||
// 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) {
|
||||
return callback(err);
|
||||
} else {
|
||||
|
||||
defineCachedRelations(obj);
|
||||
obj.__cachedRelations[relationName] = result;
|
||||
|
||||
|
|
|
@ -24,12 +24,13 @@ module.exports = function getIntrospector(ModelBuilder) {
|
|||
return 'date';
|
||||
}
|
||||
|
||||
var itemType;
|
||||
if (Array.isArray(value)) {
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
if (value[i] === null || value[i] === undefined) {
|
||||
continue;
|
||||
}
|
||||
var itemType = introspectType(value[i]);
|
||||
itemType = introspectType(value[i]);
|
||||
if (itemType) {
|
||||
return [itemType];
|
||||
}
|
||||
|
@ -43,7 +44,7 @@ module.exports = function getIntrospector(ModelBuilder) {
|
|||
|
||||
var properties = {};
|
||||
for (var p in value) {
|
||||
var itemType = introspectType(value[p]);
|
||||
itemType = introspectType(value[p]);
|
||||
if (itemType) {
|
||||
properties[p] = itemType;
|
||||
}
|
||||
|
@ -54,6 +55,7 @@ module.exports = function getIntrospector(ModelBuilder) {
|
|||
return properties;
|
||||
}
|
||||
|
||||
ModelBuilder.introspect = introspectType;
|
||||
return introspectType;
|
||||
}
|
||||
|
||||
|
|
|
@ -140,6 +140,10 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
var ModelBaseClass = parent || this.defaultModelBaseClass;
|
||||
var baseClass = settings.base || settings['super'];
|
||||
if (baseClass) {
|
||||
// Normalize base model property
|
||||
settings.base = baseClass;
|
||||
delete settings['super'];
|
||||
|
||||
if (isModelClass(baseClass)) {
|
||||
ModelBaseClass = baseClass;
|
||||
} else {
|
||||
|
@ -337,8 +341,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
}
|
||||
|
||||
// Merge the settings
|
||||
var originalSubclassSettings = 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
|
||||
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
|
||||
|
||||
|
@ -632,7 +643,7 @@ ModelBuilder.prototype.resolveType = function (type) {
|
|||
* @param {*} schemas The schemas
|
||||
* @returns {Object} A map of model constructors keyed by model name
|
||||
*/
|
||||
ModelBuilder.prototype.buildModels = function (schemas) {
|
||||
ModelBuilder.prototype.buildModels = function (schemas, createModel) {
|
||||
var models = {};
|
||||
|
||||
// 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) {
|
||||
var name = this.getSchemaName(schemas[s].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;
|
||||
relations = relations.concat(model.definition.relations);
|
||||
}
|
||||
|
|
|
@ -61,6 +61,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
|||
var properties = _extend({}, ctor.definition.properties);
|
||||
data = data || {};
|
||||
|
||||
if (typeof ctor.applyProperties === 'function') {
|
||||
ctor.applyProperties(data);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var applySetters = options.applySetters;
|
||||
var strict = options.strict;
|
||||
|
@ -130,7 +134,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
|||
}
|
||||
if (properties[p]) {
|
||||
// Managed property
|
||||
if (applySetters) {
|
||||
if (applySetters || properties[p].id) {
|
||||
self[p] = propVal;
|
||||
} else {
|
||||
self.__data[p] = propVal;
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
*/
|
||||
var assert = require('assert');
|
||||
var util = require('util');
|
||||
var utils = require('./utils');
|
||||
var i8n = require('inflection');
|
||||
var defineScope = require('./scope.js').defineScope;
|
||||
var mergeQuery = require('./scope.js').mergeQuery;
|
||||
var mergeQuery = utils.mergeQuery;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
var applyFilter = require('./connectors/memory').applyFilter;
|
||||
var ValidationError = require('./validations.js').ValidationError;
|
||||
|
|
91
lib/scope.js
91
lib/scope.js
|
@ -1,13 +1,14 @@
|
|||
var i8n = require('inflection');
|
||||
var utils = require('./utils');
|
||||
var defineCachedRelations = utils.defineCachedRelations;
|
||||
var setScopeValuesFromWhere = utils.setScopeValuesFromWhere;
|
||||
var mergeQuery = utils.mergeQuery;
|
||||
var DefaultModelBaseClass = require('./model.js');
|
||||
|
||||
/**
|
||||
* Module exports
|
||||
*/
|
||||
exports.defineScope = defineScope;
|
||||
exports.mergeQuery = mergeQuery;
|
||||
|
||||
function ScopeDefinition(definition) {
|
||||
this.isStatic = definition.isStatic;
|
||||
|
@ -168,7 +169,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
|
|||
|
||||
f._targetClass = targetModel.modelName;
|
||||
if (f._scope.collect) {
|
||||
f._targetClass = i8n.capitalize(f._scope.collect);
|
||||
f._targetClass = i8n.camelize(f._scope.collect);
|
||||
}
|
||||
|
||||
f.build = build;
|
||||
|
@ -229,35 +230,6 @@ function defineScope(cls, targetClass, name, params, methods, options) {
|
|||
|
||||
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
|
||||
function build(data) {
|
||||
data = data || {};
|
||||
|
@ -300,60 +272,3 @@ function defineScope(cls, targetClass, name, params, methods, options) {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
101
lib/utils.js
101
lib/utils.js
|
@ -3,10 +3,12 @@ exports.fieldsToArray = fieldsToArray;
|
|||
exports.selectFields = selectFields;
|
||||
exports.removeUndefined = removeUndefined;
|
||||
exports.parseSettings = parseSettings;
|
||||
exports.mergeSettings = mergeSettings;
|
||||
exports.mergeSettings = exports.deepMerge = mergeSettings;
|
||||
exports.isPlainObject = isPlainObject;
|
||||
exports.defineCachedRelations = defineCachedRelations;
|
||||
exports.sortObjectsByIds = sortObjectsByIds;
|
||||
exports.setScopeValuesFromWhere = setScopeValuesFromWhere;
|
||||
exports.mergeQuery = mergeQuery;
|
||||
|
||||
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) {
|
||||
if (!fields) return;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "loopback-datasource-juggler",
|
||||
"version": "2.9.0",
|
||||
"version": "2.10.0",
|
||||
"description": "LoopBack DataSoure Juggler",
|
||||
"keywords": [
|
||||
"StrongLoop",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -80,6 +80,17 @@ describe('include', function () {
|
|||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
Passport.find({
|
||||
include: {owner: {posts: 'author'}}
|
||||
|
@ -97,6 +108,7 @@ describe('include', function () {
|
|||
user.id.should.equal(p.ownerId);
|
||||
user.__cachedRelations.should.have.property('posts');
|
||||
user.__cachedRelations.posts.forEach(function (pp) {
|
||||
pp.should.have.property('id');
|
||||
pp.userId.should.equal(user.id);
|
||||
pp.should.have.property('author');
|
||||
pp.__cachedRelations.should.have.property('author');
|
||||
|
@ -109,6 +121,92 @@ describe('include', function () {
|
|||
});
|
||||
});
|
||||
|
||||
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) {
|
||||
User.find({include: ['posts', 'passports']}, function (err, users) {
|
||||
should.not.exist(err);
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
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 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 () {
|
||||
|
||||
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) {
|
||||
|
||||
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 schema = introspectType(json);
|
||||
|
@ -97,5 +97,33 @@ describe('Introspection of model definitions from JSON', function () {
|
|||
assert.deepEqual(obj, copy);
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -1418,7 +1418,6 @@ describe('DataAccessObject', function () {
|
|||
});
|
||||
|
||||
describe('Load models from json', function () {
|
||||
it('should be able to define models from json', function () {
|
||||
var path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
|
@ -1428,18 +1427,22 @@ describe('Load models from json', function () {
|
|||
* @returns A map of schemas keyed by name
|
||||
*/
|
||||
function loadSchemasSync(schemaFile, dataSource) {
|
||||
var modelBuilder, createModel;
|
||||
// Set up the data source
|
||||
if (!dataSource) {
|
||||
dataSource = new DataSource('memory');
|
||||
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 dataSource.modelBuilder.buildModels(schemas);
|
||||
|
||||
return modelBuilder.buildModels(schemas, createModel);
|
||||
}
|
||||
|
||||
it('should be able to define models from json', function () {
|
||||
|
||||
var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json'));
|
||||
|
||||
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 () {
|
||||
var modelBuilder = new ModelBuilder();
|
||||
|
||||
|
|
|
@ -270,6 +270,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -250,6 +250,26 @@ describe('ModelDefinition class', function () {
|
|||
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 () {
|
||||
var memory = new DataSource({connector: Memory});
|
||||
var modelBuilder = memory.modelBuilder;
|
||||
|
|
|
@ -335,7 +335,7 @@ describe('relations', function () {
|
|||
physician.patients.findById(id, function (err, ch) {
|
||||
should.not.exist(err);
|
||||
should.exist(ch);
|
||||
ch.id.should.equal(id);
|
||||
ch.id.should.eql(id);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
@ -387,7 +387,7 @@ describe('relations', function () {
|
|||
physician.patients.findById(id, function (err, ch) {
|
||||
should.not.exist(err);
|
||||
should.exist(ch);
|
||||
ch.id.should.equal(id);
|
||||
ch.id.should.eql(id);
|
||||
ch.name.should.equal('aa');
|
||||
done();
|
||||
});
|
||||
|
@ -1624,15 +1624,15 @@ describe('relations', function () {
|
|||
});
|
||||
|
||||
describe('hasAndBelongsToMany', function () {
|
||||
var Article, Tag, ArticleTag;
|
||||
var Article, TagName, ArticleTag;
|
||||
it('can be declared', function (done) {
|
||||
Article = db.define('Article', {title: String});
|
||||
Tag = db.define('Tag', {name: String});
|
||||
Article.hasAndBelongsToMany('tags');
|
||||
ArticleTag = db.models.ArticleTag;
|
||||
TagName = db.define('TagName', {name: String});
|
||||
Article.hasAndBelongsToMany('tagNames');
|
||||
ArticleTag = db.models.ArticleTagName;
|
||||
db.automigrate(function () {
|
||||
Article.destroyAll(function () {
|
||||
Tag.destroyAll(function () {
|
||||
TagName.destroyAll(function () {
|
||||
ArticleTag.destroyAll(done)
|
||||
});
|
||||
});
|
||||
|
@ -1641,11 +1641,11 @@ describe('relations', function () {
|
|||
|
||||
it('should allow to create instances on scope', function (done) {
|
||||
Article.create(function (e, article) {
|
||||
article.tags.create({name: 'popular'}, function (e, t) {
|
||||
t.should.be.an.instanceOf(Tag);
|
||||
article.tagNames.create({name: 'popular'}, function (e, t) {
|
||||
t.should.be.an.instanceOf(TagName);
|
||||
ArticleTag.findOne(function (e, 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());
|
||||
done();
|
||||
});
|
||||
|
@ -1655,11 +1655,11 @@ describe('relations', function () {
|
|||
|
||||
it('should allow to fetch scoped instances', function (done) {
|
||||
Article.findOne(function (e, article) {
|
||||
article.tags(function (e, tags) {
|
||||
article.tagNames(function (e, tags) {
|
||||
should.not.exist(e);
|
||||
should.exist(tags);
|
||||
|
||||
article.tags().should.eql(tags);
|
||||
article.tagNames().should.eql(tags);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -1668,12 +1668,12 @@ describe('relations', function () {
|
|||
|
||||
it('should allow to add connection with instance', function (done) {
|
||||
Article.findOne(function (e, article) {
|
||||
Tag.create({name: 'awesome'}, function (e, tag) {
|
||||
article.tags.add(tag, function (e, at) {
|
||||
TagName.create({name: 'awesome'}, function (e, tag) {
|
||||
article.tagNames.add(tag, function (e, at) {
|
||||
should.not.exist(e);
|
||||
should.exist(at);
|
||||
at.should.be.an.instanceOf(ArticleTag);
|
||||
at.tagId.should.equal(tag.id);
|
||||
at.tagNameId.should.equal(tag.id);
|
||||
at.articleId.should.equal(article.id);
|
||||
done();
|
||||
});
|
||||
|
@ -1683,12 +1683,12 @@ describe('relations', function () {
|
|||
|
||||
it('should allow to remove connection with instance', function (done) {
|
||||
Article.findOne(function (e, article) {
|
||||
article.tags(function (e, tags) {
|
||||
article.tagNames(function (e, tags) {
|
||||
var len = tags.length;
|
||||
tags.should.not.be.empty;
|
||||
article.tags.remove(tags[0], function (e) {
|
||||
article.tagNames.remove(tags[0], function (e) {
|
||||
should.not.exist(e);
|
||||
article.tags(true, function (e, tags) {
|
||||
article.tagNames(true, function (e, tags) {
|
||||
tags.should.have.lengthOf(len - 1);
|
||||
done();
|
||||
});
|
||||
|
@ -1698,7 +1698,7 @@ describe('relations', function () {
|
|||
});
|
||||
|
||||
it('should set targetClass on scope property', function() {
|
||||
should.equal(Article.prototype.tags._targetClass, 'Tag');
|
||||
should.equal(Article.prototype.tagNames._targetClass, 'TagName');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue