Merge branch 'release/2.8.0' into production

This commit is contained in:
Raymond Feng 2014-09-04 10:31:29 -07:00
commit 3753e36da7
19 changed files with 800 additions and 116 deletions

View File

@ -6,7 +6,7 @@ var fs = require('fs');
var async = require('async');
/**
* Initialize the Oracle connector against the given data source
* Initialize the Memory connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
@ -406,9 +406,10 @@ function applyFilter(filter) {
if (typeof value === 'string' && (example instanceof RegExp)) {
return value.match(example);
}
if (example === undefined || value === undefined) {
if (example === undefined) {
return undefined;
}
if (typeof example === 'object') {
// ignore geo near filter
if (example.near) {
@ -425,6 +426,10 @@ function applyFilter(filter) {
return false;
}
if ('neq' in example) {
return compare(example.neq, value) !== 0;
}
if (example.like || example.nlike) {
var like = example.like || example.nlike;
@ -445,7 +450,8 @@ function applyFilter(filter) {
}
}
// not strict equality
return (example !== null ? example.toString() : example) == (value !== null ? value.toString() : value);
return (example !== null ? example.toString() : example)
== (value != null ? value.toString() : value);
}
/**

144
lib/connectors/transient.js Normal file
View File

@ -0,0 +1,144 @@
var util = require('util');
var Connector = require('loopback-connector').Connector;
var utils = require('../utils');
var crypto = require('crypto');
/**
* Initialize the Transient connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new Transient(null, dataSource.settings);
dataSource.connector.connect(callback);
};
exports.Transient = Transient;
function Transient(m, settings) {
settings = settings || {};
if (typeof settings.generateId === 'function') {
this.generateId = settings.generateId.bind(this);
}
this.defaultIdType = settings.defaultIdType || String;
if (m instanceof Transient) {
this.isTransaction = true;
this.constructor.super_.call(this, 'transient', settings);
this._models = m._models;
} else {
this.isTransaction = false;
this.constructor.super_.call(this, 'transient', settings);
}
}
util.inherits(Transient, Connector);
Transient.prototype.getDefaultIdType = function() {
return this.defaultIdType;
};
Transient.prototype.getTypes = function() {
return ['db', 'nosql', 'transient'];
};
Transient.prototype.connect = function (callback) {
if (this.isTransaction) {
this.onTransactionExec = callback;
} else {
process.nextTick(callback);
}
};
Transient.prototype.generateId = function(model, data, idName) {
var idType;
var props = this._models[model].properties;
if (idName) idType = props[idName] && props[idName].type;
idType = idType || this.getDefaultIdType();
if (idType === Number) {
return Math.floor(Math.random() * 10000); // max. 4 digits
} else {
return crypto.randomBytes(Math.ceil(24/2))
.toString('hex') // convert to hexadecimal format
.slice(0, 24); // return required number of characters
}
};
Transient.prototype.exists = function exists(model, id, callback) {
process.nextTick(function () { callback(null, false); }.bind(this));
};
Transient.prototype.find = function find(model, id, callback) {
process.nextTick(function () { callback(null, null); }.bind(this));
};
Transient.prototype.all = function all(model, filter, callback) {
process.nextTick(function () { callback(null, []); });
};
Transient.prototype.count = function count(model, callback, where) {
process.nextTick(function () { callback(null, 0); });
};
Transient.prototype.create = function create(model, data, callback) {
var props = this._models[model].properties;
var idName = this.idName(model);
if (idName && props[idName]) {
var id = this.getIdValue(model, data) || this.generateId(model, data, idName);
id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id);
}
this.flush('create', id, callback);
};
Transient.prototype.save = function save(model, data, callback) {
this.flush('save', data, callback);
};
Transient.prototype.update =
Transient.prototype.updateAll = function updateAll(model, where, data, cb) {
this.flush('update', null, cb);
};
Transient.prototype.updateAttributes = function updateAttributes(model, id, data, cb) {
if (!id) {
var err = new Error('You must provide an id when updating attributes!');
if (cb) {
return cb(err);
} else {
throw err;
}
}
this.setIdValue(model, data, id);
this.save(model, data, cb);
};
Transient.prototype.destroy = function destroy(model, id, callback) {
this.flush('destroy', null, callback);
};
Transient.prototype.destroyAll = function destroyAll(model, where, callback) {
if (!callback && 'function' === typeof where) {
callback = where;
where = undefined;
}
this.flush('destroyAll', null, callback);
};
/*!
* Flush the cache - noop.
* @param {Function} callback
*/
Transient.prototype.flush = function (action, result, callback) {
process.nextTick(function () { callback && callback(null, result); });
};
Transient.prototype.transaction = function () {
return new Transient(this);
};
Transient.prototype.exec = function (callback) {
this.onTransactionExec();
setTimeout(callback, 50);
};

View File

@ -8,8 +8,7 @@ module.exports = DataAccessObject;
* Module dependencies
*/
var jutil = require('./jutil');
var validations = require('./validations.js');
var ValidationError = validations.ValidationError;
var ValidationError = require('./validations').ValidationError;
var Relation = require('./relations.js');
var Inclusion = require('./include.js');
var List = require('./list.js');
@ -40,15 +39,12 @@ function DataAccessObject() {
}
}
function idName(m) {
return m.getDataSource().idName
? m.getDataSource().idName(m.modelName) : 'id';
return m.definition.idName() || 'id';
}
function getIdValue(m, data) {
return data && data[m.getDataSource().idName(m.modelName)];
return data && data[idName(m)];
}
function setIdValue(m, data, value) {
@ -331,7 +327,7 @@ DataAccessObject.findByIds = function(ids, cond, cb) {
cond = {};
}
var pk = this.dataSource.idName(this.modelName) || 'id';
var pk = idName(this);
if (ids.length === 0) {
process.nextTick(function() { cb(null, []); });
return;

View File

@ -392,6 +392,7 @@ DataSource.prototype.defineScopes = function (modelClass, scopes) {
* @param relations
*/
DataSource.prototype.defineRelations = function (modelClass, relations) {
var self = this;
// Create a function for the closure in the loop
var createListener = function (name, relation, targetModel, throughModel) {
@ -416,7 +417,7 @@ DataSource.prototype.defineRelations = function (modelClass, relations) {
throughModel.once('dataAccessConfigured', function (model) {
if (isModelDataSourceAttached(targetModel)) {
// The target model is resolved
var params = traverse(relations).clone();
var params = traverse(relation).clone();
params.as = name;
params.model = targetModel;
params.through = model;
@ -428,7 +429,7 @@ DataSource.prototype.defineRelations = function (modelClass, relations) {
// Set up the relations
if (relations) {
for (var rn in relations) {
Object.keys(relations).forEach(function(rn) {
var r = relations[rn];
assert(DataSource.relationTypes.indexOf(r.type) !== -1, "Invalid relation type: " + r.type);
var targetModel, polymorphicName;
@ -447,12 +448,12 @@ DataSource.prototype.defineRelations = function (modelClass, relations) {
}
if (r.model) {
targetModel = isModelClass(r.model) ? r.model : this.getModel(r.model, true);
targetModel = isModelClass(r.model) ? r.model : self.getModel(r.model, true);
}
var throughModel = null;
if (r.through) {
throughModel = isModelClass(r.through) ? r.through : this.getModel(r.through, true);
throughModel = isModelClass(r.through) ? r.through : self.getModel(r.through, true);
}
if ((targetModel && !isModelDataSourceAttached(targetModel))
@ -469,7 +470,7 @@ DataSource.prototype.defineRelations = function (modelClass, relations) {
}
modelClass[r.type].call(modelClass, rn, params);
}
}
});
}
};

View File

@ -210,6 +210,7 @@ ModelDefinition.prototype.build = function (forceRebuild) {
this.properties = null;
this.relations = [];
this._ids = null;
this.json = null;
}
if (this.properties) {
return this.properties;

View File

@ -11,7 +11,7 @@ var util = require('util');
var jutil = require('./jutil');
var List = require('./list');
var Hookable = require('./hooks');
var validations = require('./validations.js');
var validations = require('./validations');
var _extend = util._extend;
// Set up an object for quick lookup

View File

@ -391,13 +391,13 @@ util.inherits(HasOne, Relation);
* EmbedsOne subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {EmbedsMany}
* @returns {EmbedsOne}
* @constructor
* @class EmbedsOne
*/
function EmbedsOne(definition, modelInstance) {
if (!(this instanceof EmbedsOne)) {
return new EmbedsMany(definition, modelInstance);
return new EmbedsOne(definition, modelInstance);
}
assert(definition.type === RelationTypes.embedsOne);
Relation.apply(this, arguments);
@ -489,7 +489,6 @@ function lookupModelTo(modelFrom, modelTo, params, singularize) {
}
if (typeof modelTo === 'string') {
modelToName = (singularize ? i8n.singularize(params.as) : params.as).toLowerCase();
console.log(modelToName)
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName) || modelTo;
}
if (typeof modelTo !== 'function') {
@ -504,9 +503,9 @@ function lookupModelTo(modelFrom, modelTo, params, singularize) {
* @param {Object|String} params Name of the polymorphic relation or params
* @returns {Object} The normalized parameters
*/
function polymorphicParams(params) {
function polymorphicParams(params, as) {
if (typeof params === 'string') params = { as: params };
if (typeof params.as !== 'string') params.as = 'reference'; // default
if (typeof params.as !== 'string') params.as = as || 'reference'; // default
params.foreignKey = params.foreignKey || i8n.camelize(params.as + '_id', true);
params.discriminator = params.discriminator || i8n.camelize(params.as + '_type', true);
return params;
@ -541,13 +540,17 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
var keyThrough = params.keyThrough || i8n.camelize(modelTo.modelName + '_id', true);
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
var discriminator, polymorphic;
if (params.polymorphic) {
polymorphic = polymorphicParams(params.polymorphic);
if (params.invert) polymorphic.invert = true;
if (params.invert) {
polymorphic.invert = true;
keyThrough = polymorphic.foreignKey;
}
discriminator = polymorphic.discriminator;
if (!params.invert) {
fk = polymorphic.foreignKey;
@ -568,14 +571,12 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
properties: params.properties,
scope: params.scope,
options: params.options,
keyThrough: keyThrough,
polymorphic: polymorphic
});
definition.modelThrough = params.through;
var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true);
definition.keyThrough = keyThrough;
modelFrom.relations[relationName] = definition;
if (!params.through) {
@ -636,16 +637,34 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
filter.where[fk] = this[idName];
definition.applyScope(this, filter);
if (params.through && params.polymorphic && params.invert) {
filter.where[discriminator] = modelTo.modelName; // overwrite
filter.collect = params.polymorphic;
filter.include = filter.collect;
} else if (params.through) {
filter.collect = i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
if (definition.modelThrough) {
var throughRelationName;
// find corresponding belongsTo relations from through model as collect
for(var r in definition.modelThrough.relations) {
var relation = definition.modelThrough.relations[r];
// should be a belongsTo and match modelTo and keyThrough
// if relation is polymorphic then check keyThrough only
if (relation.type === RelationTypes.belongsTo &&
(relation.polymorphic && !relation.modelTo || relation.modelTo === definition.modelTo) &&
(relation.keyFrom === definition.keyThrough)
) {
throughRelationName = relation.name;
break;
}
}
if (definition.polymorphic && definition.polymorphic.invert) {
filter.collect = definition.polymorphic.as;
filter.include = filter.collect;
} else {
filter.collect = throughRelationName || i8n.camelize(modelTo.modelName, true);
filter.include = filter.collect;
}
}
return filter;
}, scopeMethods, definition.options);
@ -918,14 +937,18 @@ HasManyThrough.prototype.create = function create(data, done) {
/**
* Add the target model instance to the 'hasMany' relation
* @param {Object|ID} acInst The actual instance or id value
* @param {Object} [data] Optional data object for the through model to be created
*/
HasManyThrough.prototype.add = function (acInst, done) {
HasManyThrough.prototype.add = function (acInst, data, done) {
var self = this;
var definition = this.definition;
var modelThrough = definition.modelThrough;
var pk1 = definition.keyFrom;
var data = {};
if (typeof data === 'function') {
done = data;
data = {};
}
var query = {};
// The primary key for the target model
@ -1056,21 +1079,23 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
var idName, relationName, fk;
if (params.polymorphic) {
relationName = params.as || (typeof modelTo === 'string' ? modelTo : null); // initially
if (params.polymorphic === true) {
// modelTo arg will be the name of the polymorphic relation (string)
polymorphic = polymorphicParams(modelTo);
polymorphic = polymorphicParams(modelTo, relationName);
} else {
polymorphic = polymorphicParams(params.polymorphic);
polymorphic = polymorphicParams(params.polymorphic, relationName);
}
modelTo = null; // will lookup dynamically
idName = params.idName || 'id';
relationName = params.as || polymorphic.as;
relationName = params.as || polymorphic.as; // finally
fk = polymorphic.foreignKey;
discriminator = polymorphic.discriminator;
if (typeof polymorphic.idType === 'string') { // explicit key type
if (polymorphic.idType) { // explicit key type
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, { type: polymorphic.idType, index: true });
} else { // try to use the same foreign key type as modelFrom
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelFrom.modelName);
@ -1842,8 +1867,9 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
type: [modelTo], default: function() { return []; }
});
// unique id is required
modelTo.validatesPresenceOf(idName);
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
modelTo.validatesPresenceOf(idName); // unique id is required
}
if (!params.polymorphic) {
modelFrom.validate(propertyName, function(err) {
@ -1853,7 +1879,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
return ids.indexOf(id) === pos;
});
if (ids.length !== uniqueIds.length) {
this.errors.add(propertyName, 'Contains duplicate `' + idName + '`', 'uniqueness');
this.errors.add(propertyName, 'contains duplicate `' + idName + '`', 'uniqueness');
err(false);
}
}, { code: 'uniqueness' })
@ -1877,7 +1903,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
}
} else {
hasErrors = true;
self.errors.add(propertyName, 'Contains invalid item', 'invalid');
self.errors.add(propertyName, 'contains invalid item', 'invalid');
}
});
if (hasErrors) err(false);
@ -2143,7 +2169,6 @@ EmbedsMany.prototype.create = function (targetModelData, cb) {
var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
var modelInstance = this.modelInstance;
var autoId = this.definition.options.autoId !== false;
if (typeof targetModelData === 'function' && !cb) {
cb = targetModelData;
@ -2170,16 +2195,22 @@ EmbedsMany.prototype.create = function (targetModelData, cb) {
};
EmbedsMany.prototype.build = function(targetModelData) {
var pk = this.definition.keyTo;
var modelTo = this.definition.modelTo;
var modelInstance = this.modelInstance;
var autoId = this.definition.options.autoId !== false;
var forceId = this.definition.options.forceId;
var connector = modelTo.dataSource.connector;
var pk = this.definition.keyTo;
var pkProp = modelTo.definition.properties[pk]
var pkType = pkProp && pkProp.type;
var embeddedList = this.embeddedList();
targetModelData = targetModelData || {};
if (typeof targetModelData[pk] !== 'number' && autoId) {
var assignId = (forceId || targetModelData[pk] === undefined);
if (assignId && pkType === Number) {
var ids = embeddedList.map(function(m) {
return (typeof m[pk] === 'number' ? m[pk] : 0);
});
@ -2188,6 +2219,9 @@ EmbedsMany.prototype.build = function(targetModelData) {
} else {
targetModelData[pk] = 1;
}
} else if (assignId && typeof connector.generateId === 'function') {
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
targetModelData[pk] = id;
}
this.definition.applyProperties(modelInstance, targetModelData);
@ -2325,7 +2359,7 @@ RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo,
return ids.indexOf(id) === pos;
});
if (ids.length !== uniqueIds.length) {
var msg = 'Contains duplicate `' + modelTo.modelName + '` instance';
var msg = 'contains duplicate `' + modelTo.modelName + '` instance';
this.errors.add(relationName, msg, 'uniqueness');
err(false);
}

View File

@ -1,6 +1,8 @@
var i8n = require('inflection');
var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations;
var DefaultModelBaseClass = require('./model.js');
/**
* Module exports
*/
@ -13,10 +15,26 @@ function ScopeDefinition(definition) {
this.modelTo = definition.modelTo || definition.modelFrom;
this.name = definition.name;
this.params = definition.params;
this.methods = definition.methods;
this.options = definition.options;
this.methods = definition.methods || {};
this.options = definition.options || {};
}
ScopeDefinition.prototype.targetModel = function(receiver) {
if (typeof this.options.modelTo === 'function') {
var modelTo = this.options.modelTo.call(this, receiver) || this.modelTo;
} else {
var modelTo = this.modelTo;
}
if (!(modelTo.prototype instanceof DefaultModelBaseClass)) {
var msg = 'Invalid target model for scope `';
msg += (this.isStatic ? this.modelFrom : this.modelFrom.constructor).modelName;
msg += this.isStatic ? '.' : '.prototype.';
msg += this.name + '`.';
throw new Error(msg);
}
return modelTo;
};
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
var name = this.name;
var self = receiver;
@ -42,7 +60,8 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres
|| actualRefresh) {
// It either doesn't hit the cache or refresh is required
var params = mergeQuery(actualCond, scopeParams);
return this.modelTo.find(params, function (err, data) {
var targetModel = this.targetModel(receiver);
return targetModel.find(params, function (err, data) {
if (!err && saveOnCache) {
defineCachedRelations(self);
self.__cachedRelations[name] = data;
@ -74,7 +93,6 @@ ScopeDefinition.prototype.defineMethod = function(name, fn) {
* @param methods An object of methods keyed by the method name to be bound to the class
*/
function defineScope(cls, targetClass, name, params, methods, options) {
// collect meta info about scope
if (!cls._scopeMeta) {
cls._scopeMeta = {};
@ -84,7 +102,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
// are same
if (cls === targetClass) {
cls._scopeMeta[name] = params;
} else {
} else if (targetClass) {
if (!targetClass._scopeMeta) {
targetClass._scopeMeta = {};
}
@ -100,7 +118,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
name: name,
params: params,
methods: methods,
options: options || {}
options: options
});
if(isStatic) {
@ -127,7 +145,9 @@ function defineScope(cls, targetClass, name, params, methods, options) {
*
*/
get: function () {
var targetModel = definition.targetModel(this);
var self = this;
var f = function(condOrRefresh, cb) {
if(arguments.length === 1) {
definition.related(self, f._scope, condOrRefresh);
@ -135,15 +155,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
definition.related(self, f._scope, condOrRefresh, cb);
}
};
f._receiver = this;
f._scope = typeof definition.params === 'function' ?
definition.params.call(self) : definition.params;
f._targetClass = definition.modelTo.modelName;
f._targetClass = targetModel.modelName;
if (f._scope.collect) {
f._targetClass = i8n.capitalize(f._scope.collect);
}
f.build = build;
f.create = create;
f.destroyAll = destroyAll;
@ -151,6 +172,8 @@ function defineScope(cls, targetClass, name, params, methods, options) {
for (var i in definition.methods) {
f[i] = definition.methods[i].bind(self);
}
if (!targetClass) return f;
// Define scope-chaining, such as
// Station.scope('active', {where: {isActive: true}});
@ -160,7 +183,7 @@ function defineScope(cls, targetClass, name, params, methods, options) {
Object.defineProperty(f, name, {
enumerable: false,
get: function () {
mergeQuery(f._scope, targetClass._scopeMeta[name]);
mergeQuery(f._scope, targetModel._scopeMeta[name]);
return f;
}
});
@ -207,16 +230,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
* @param {Object} The data object
* @param {Object} The where clause
*/
function setScopeValuesFromWhere(data, where) {
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]);
setScopeValuesFromWhere(data, where[i][w], targetModel);
}
continue;
}
var prop = targetClass.definition.properties[i];
var prop = targetModel.definition.properties[i];
if (prop) {
var val = where[i];
if (typeof val !== 'object' || val instanceof prop.type
@ -233,9 +256,10 @@ function defineScope(cls, targetClass, name, params, methods, options) {
function build(data) {
data = data || {};
// Find all fixed property values for the scope
var targetModel = definition.targetModel(this._receiver);
var where = (this._scope && this._scope.where) || {};
setScopeValuesFromWhere(data, where);
return new targetClass(data);
setScopeValuesFromWhere(data, where, targetModel);
return new targetModel(data);
}
function create(data, cb) {
@ -256,14 +280,16 @@ function defineScope(cls, targetClass, name, params, methods, options) {
if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetClass.destroyAll(filter.where, cb);
var targetModel = definition.targetModel(this._receiver);
targetModel.destroyAll(filter.where, cb);
}
function count(where, cb) {
if (typeof where === 'function') cb = where, where = {};
var scoped = (this._scope && this._scope.where) || {};
var filter = mergeQuery({ where: scoped }, { where: where || {} });
targetClass.count(filter.where, cb);
var targetModel = definition.targetModel(this._receiver);
targetModel.count(filter.where, cb);
}
return definition;

View File

@ -1,4 +1,6 @@
var util = require('util');
var extend = util._extend;
/*!
* Module exports
*/
@ -369,7 +371,9 @@ var validators = {
function getConfigurator(name, opts) {
return function () {
configure(this, name, arguments, opts);
var args = Array.prototype.slice.call(arguments);
args[1] = args[1] || {};
configure(this, name, args, opts);
};
}
@ -407,9 +411,10 @@ function getConfigurator(name, opts) {
*/
Validatable.prototype.isValid = function (callback, data) {
var valid = true, inst = this, wait = 0, async = false;
var validations = this.constructor.validations;
// exit with success when no errors
if (!this.constructor._validations) {
if (typeof validations !== 'object') {
cleanErrors(this);
if (callback) {
this.trigger('validate', function (validationsDone) {
@ -431,21 +436,25 @@ Validatable.prototype.isValid = function (callback, data) {
var inst = this,
asyncFail = false;
this.constructor._validations.forEach(function (v) {
if (v[2] && v[2].async) {
async = true;
wait += 1;
process.nextTick(function () {
validationFailed(inst, v, done);
});
} else {
if (validationFailed(inst, v)) {
valid = false;
var attrs = Object.keys(validations || {});
attrs.forEach(function(attr) {
var attrValidations = validations[attr] || [];
attrValidations.forEach(function(v) {
if (v.options && v.options.async) {
async = true;
wait += 1;
process.nextTick(function () {
validationFailed(inst, attr, v, done);
});
} else {
if (validationFailed(inst, attr, v)) {
valid = false;
}
}
}
});
});
if (!async) {
validationsDone.call(inst, function () {
if (valid) cleanErrors(inst);
@ -487,11 +496,9 @@ function cleanErrors(inst) {
});
}
function validationFailed(inst, v, cb) {
var attr = v[0];
var conf = v[1];
var opts = v[2] || {};
function validationFailed(inst, attr, conf, cb) {
var opts = conf.options || {};
if (typeof attr !== 'string') return false;
// here we should check skip validation conditions (if, unless)
@ -615,12 +622,12 @@ function blank(v) {
}
function configure(cls, validation, args, opts) {
if (!cls._validations) {
Object.defineProperty(cls, '_validations', {
if (!cls.validations) {
Object.defineProperty(cls, 'validations', {
writable: true,
configurable: true,
enumerable: false,
value: []
value: {}
});
}
args = [].slice.call(args);
@ -634,9 +641,13 @@ function configure(cls, validation, args, opts) {
conf.customValidator = args.pop();
}
conf.validation = validation;
args.forEach(function (attr) {
cls._validations.push([attr, conf, opts]);
});
var attr = args[0];
if (typeof attr === 'string') {
var validation = extend({}, conf);
validation.options = opts || {};
cls.validations[attr] = cls.validations[attr] || [];
cls.validations[attr].push(validation);
}
}
function Errors() {

View File

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

View File

@ -12,10 +12,17 @@ module.exports = require('should');
}
*/
var ModelBuilder = require('../').ModelBuilder;
var Schema = require('../').Schema;
if (!('getSchema' in global)) {
global.getSchema = function () {
return new Schema('memory');
global.getSchema = function (connector, settings) {
return new Schema(connector || 'memory', settings);
};
}
if (!('getModelBuilder' in global)) {
global.getModelBuilder = function () {
return new ModelBuilder();
};
}

View File

@ -1110,6 +1110,27 @@ describe('Load models with relations', function () {
done();
});
it('should handle hasMany through options', function (done) {
var ds = new DataSource('memory');
var Physician = ds.createModel('Physician', {
name: String
}, {relations: {patients: {model: 'Patient', type: 'hasMany', foreignKey: 'leftId', through: 'Appointment'}}});
var Patient = ds.createModel('Patient', {
name: String
}, {relations: {physicians: {model: 'Physician', type: 'hasMany', foreignKey: 'rightId', through: 'Appointment'}}});
var Appointment = ds.createModel('Appointment', {
physicianId: Number,
patientId: Number,
appointmentDate: Date
}, {relations: {patient: {type: 'belongsTo', model: 'Patient'}, physician: {type: 'belongsTo', model: 'Physician'}}});
assert(Physician.relations['patients'].keyTo === 'leftId');
assert(Patient.relations['physicians'].keyTo === 'rightId');
done();
});
it('should set up relations after attach', function (done) {
var ds = new DataSource('memory');
var modelBuilder = new ModelBuilder();

View File

@ -102,7 +102,7 @@ describe('manipulation', function () {
Person.validatesPresenceOf('name');
Person.create(batch,function (errors, persons) {
delete Person._validations;
delete Person.validations;
should.exist(errors);
errors.should.have.lengthOf(batch.length);
should.not.exist(errors[0]);

View File

@ -199,6 +199,41 @@ describe('Memory connector', function () {
});
});
it('should support neq operator for number', function (done) {
User.find({where: {order: {neq: 6}}}, function (err, users) {
should.not.exist(err);
users.length.should.be.equal(5);
for (var i = 0; i < users.length; i++) {
users[i].order.should.not.be.equal(6);
}
done();
});
});
it('should support neq operator for string', function (done) {
User.find({where: {role: {neq: 'lead'}}}, function (err, users) {
should.not.exist(err);
users.length.should.be.equal(4);
for (var i = 0; i < users.length; i++) {
if (users[i].role) {
users[i].role.not.be.equal('lead');
}
}
done();
});
});
it('should support neq operator for null', function (done) {
User.find({where: {role: {neq: null}}}, function (err, users) {
should.not.exist(err);
users.length.should.be.equal(2);
for (var i = 0; i < users.length; i++) {
should.exist(users[i].role);
}
done();
});
});
function seed(done) {
var beatles = [
{

View File

@ -55,6 +55,8 @@ describe('ModelDefinition class', function () {
});
User.build();
var json = User.toJSON();
User.defineProperty("id", {type: "number", id: true});
assert.equal(User.properties.name.type, String);
@ -62,8 +64,12 @@ describe('ModelDefinition class', function () {
assert.equal(User.properties.approved.type, Boolean);
assert.equal(User.properties.joinedAt.type, Date);
assert.equal(User.properties.age.type, Number);
assert.equal(User.properties.id.type, Number);
json = User.toJSON();
assert.deepEqual(json.properties.id, {type: 'Number', id: true});
done();
});

View File

@ -1,12 +1,18 @@
// This test written in mocha+should.js
var should = require('./init.js');
var jdb = require('../');
var DataSource = jdb.DataSource;
var db, Book, Chapter, Author, Reader;
var db, tmp, Book, Chapter, Author, Reader;
var Category, Job;
var Picture, PictureLink;
var Person, Address;
var Link;
var getTransientDataSource = function(settings) {
return new DataSource('transient', settings, db.modelBuilder);
};
describe('relations', function () {
describe('hasMany', function () {
@ -433,6 +439,24 @@ describe('relations', function () {
});
});
it('should allow to add connection with through data', function (done) {
Physician.create({name: 'ph1'}, function (e, physician) {
Patient.create({name: 'pa1'}, function (e, patient) {
var now = Date.now();
physician.patients.add(patient, { date: new Date(now) }, function (e, app) {
should.not.exist(e);
should.exist(app);
app.should.be.an.instanceOf(Appointment);
app.physicianId.should.equal(physician.id);
app.patientId.should.equal(patient.id);
app.patientId.should.equal(patient.id);
app.date.getTime().should.equal(now);
done();
});
});
});
});
it('should allow to remove connection with instance', function (done) {
var id;
Physician.create(function (err, physician) {
@ -462,7 +486,159 @@ describe('relations', function () {
});
});
describe('hasMany through - collect', function () {
var Physician, Patient, Appointment, Address;
beforeEach(function (done) {
db = getSchema();
Physician = db.define('Physician', {name: String});
Patient = db.define('Patient', {name: String});
Appointment = db.define('Appointment', {date: {type: Date,
default: function () {
return new Date();
}}});
Address = db.define('Address', {name: String});
db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'], function (err) {
done(err);
});
});
describe('with default options', function () {
it('can determine the collect by modelTo\'s name as default', function () {
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
Patient.belongsTo(Address);
Appointment.belongsTo(Physician);
Appointment.belongsTo(Patient);
var physician = new Physician({id: 1});
var scope1 = physician.patients._scope;
scope1.should.have.property('collect', 'patient');
scope1.should.have.property('include', 'patient');
var patient = new Patient({id: 1});
var scope2 = patient.yyy._scope;
scope2.should.have.property('collect', 'physician');
scope2.should.have.property('include', 'physician');
});
});
describe('when custom reverse belongsTo names for both sides', function () {
it('can determine the collect via keyThrough', function () {
Physician.hasMany(Patient, {through: Appointment, foreignKey: 'fooId', keyThrough: 'barId'});
Patient.hasMany(Physician, {through: Appointment, foreignKey: 'barId', keyThrough: 'fooId', as: 'yyy'});
Appointment.belongsTo(Physician, {as: 'foo'});
Appointment.belongsTo(Patient, {as: 'bar'});
Patient.belongsTo(Address); // jam.
Appointment.belongsTo(Patient, {as: 'car'}); // jam. Should we complain in this case???
var physician = new Physician({id: 1});
var scope1 = physician.patients._scope;
scope1.should.have.property('collect', 'bar');
scope1.should.have.property('include', 'bar');
var patient = new Patient({id: 1});
var scope2 = patient.yyy._scope;
scope2.should.have.property('collect', 'foo');
scope2.should.have.property('include', 'foo');
});
it('can determine the collect via modelTo name', function () {
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
Appointment.belongsTo(Physician, {as: 'foo', foreignKey: 'physicianId'});
Appointment.belongsTo(Patient, {as: 'bar', foreignKey: 'patientId'});
Patient.belongsTo(Address); // jam.
var physician = new Physician({id: 1});
var scope1 = physician.patients._scope;
scope1.should.have.property('collect', 'bar');
scope1.should.have.property('include', 'bar');
var patient = new Patient({id: 1});
var scope2 = patient.yyy._scope;
scope2.should.have.property('collect', 'foo');
scope2.should.have.property('include', 'foo');
});
it('can determine the collect via modelTo name (with jams)', function () {
Physician.hasMany(Patient, {through: Appointment});
Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
Appointment.belongsTo(Physician, {as: 'foo', foreignKey: 'physicianId'});
Appointment.belongsTo(Patient, {as: 'bar', foreignKey: 'patientId'});
Patient.belongsTo(Address); // jam.
Appointment.belongsTo(Physician, {as: 'goo', foreignKey: 'physicianId'}); // jam. Should we complain in this case???
Appointment.belongsTo(Patient, {as: 'car', foreignKey: 'patientId'}); // jam. Should we complain in this case???
var physician = new Physician({id: 1});
var scope1 = physician.patients._scope;
scope1.should.have.property('collect', 'bar');
scope1.should.have.property('include', 'bar');
var patient = new Patient({id: 1});
var scope2 = patient.yyy._scope;
scope2.should.have.property('collect', 'foo'); // first matched relation
scope2.should.have.property('include', 'foo'); // first matched relation
});
});
describe('when custom reverse belongsTo name for one side only', function () {
beforeEach(function () {
Physician.hasMany(Patient, {as: 'xxx', through: Appointment, foreignKey: 'fooId'});
Patient.hasMany(Physician, {as: 'yyy', through: Appointment, keyThrough: 'fooId'});
Appointment.belongsTo(Physician, {as: 'foo'});
Appointment.belongsTo(Patient);
Patient.belongsTo(Address); // jam.
Appointment.belongsTo(Physician, {as: 'bar'}); // jam. Should we complain in this case???
});
it('can determine the collect via model name', function () {
var physician = new Physician({id: 1});
var scope1 = physician.xxx._scope;
scope1.should.have.property('collect', 'patient');
scope1.should.have.property('include', 'patient');
});
it('can determine the collect via keyThrough', function () {
var patient = new Patient({id: 1});
var scope2 = patient.yyy._scope;
scope2.should.have.property('collect', 'foo');
scope2.should.have.property('include', 'foo');
});
});
});
describe('hasMany through - between same model', function () {
var User, Follow, Address;
before(function (done) {
db = getSchema();
User = db.define('User', {name: String});
Follow = db.define('Follow', {date: {type: Date,
default: function () {
return new Date();
}}});
Address = db.define('Address', {name: String});
User.hasMany(User, {as: 'followers', foreignKey: 'followeeId', keyThrough: 'followerId', through: Follow});
User.hasMany(User, {as: 'following', foreignKey: 'followerId', keyThrough: 'followeeId', through: Follow});
User.belongsTo(Address);
Follow.belongsTo(User, {as: 'follower'});
Follow.belongsTo(User, {as: 'followee'});
db.automigrate(['User', 'Follow', 'Address'], function (err) {
done(err);
});
});
it('can determine the collect via keyThrough for each side', function () {
var user = new User({id: 1});
var scope1 = user.followers._scope;
scope1.should.have.property('collect', 'follower');
scope1.should.have.property('include', 'follower');
var scope2 = user.following._scope;
scope2.should.have.property('collect', 'followee');
scope2.should.have.property('include', 'followee');
});
});
describe('hasMany with properties', function () {
it('can be declared with properties', function (done) {
Book.hasMany(Chapter, { properties: { type: 'bookType' } });
@ -915,6 +1091,29 @@ describe('relations', function () {
db.automigrate(done);
});
it('can determine the collect via modelTo name', function () {
Author.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' });
Reader.hasAndBelongsToMany(Picture, { through: PictureLink, polymorphic: 'imageable' });
// Optionally, define inverse relations:
Picture.hasMany(Author, { through: PictureLink, polymorphic: 'imageable', invert: true });
Picture.hasMany(Reader, { through: PictureLink, polymorphic: 'imageable', invert: true });
var author = new Author({id: 1});
var scope1 = author.pictures._scope;
scope1.should.have.property('collect', 'picture');
scope1.should.have.property('include', 'picture');
var reader = new Reader({id: 1});
var scope2 = reader.pictures._scope;
scope2.should.have.property('collect', 'picture');
scope2.should.have.property('include', 'picture');
var picture = new Picture({id: 1});
var scope3 = picture.authors._scope;
scope3.should.have.property('collect', 'imageable');
scope3.should.have.property('include', 'imageable');
var scope4 = picture.readers._scope;
scope4.should.have.property('collect', 'imageable');
scope4.should.have.property('include', 'imageable');
});
var author, reader, pictures = [];
it('should create polymorphic relation - author', function (done) {
Author.create({ name: 'Author 1' }, function (err, a) {
@ -1486,9 +1685,10 @@ describe('relations', function () {
var Other;
before(function () {
tmp = getTransientDataSource();
db = getSchema();
Person = db.define('Person', {name: String});
Passport = db.define('Passport',
Passport = tmp.define('Passport',
{name:{type:'string', required: true}},
{idInjection: false}
);
@ -1634,9 +1834,10 @@ describe('relations', function () {
var address1, address2;
before(function (done) {
tmp = getTransientDataSource({defaultIdType: Number});
db = getSchema();
Person = db.define('Person', {name: String});
Address = db.define('Address', {street: String});
Address = tmp.define('Address', {street: String});
Address.validatesPresenceOf('street');
db.automigrate(function () {
@ -1813,9 +2014,10 @@ describe('relations', function () {
describe('embedsMany - explicit ids', function () {
before(function (done) {
tmp = getTransientDataSource();
db = getSchema();
Person = db.define('Person', {name: String});
Address = db.define('Address', {id: { type: String, id: true }, street: String});
Address = tmp.define('Address', {street: String});
Address.validatesPresenceOf('street');
db.automigrate(function () {
@ -1824,13 +2026,13 @@ describe('relations', function () {
});
it('can be declared', function (done) {
Person.embedsMany(Address, { options: { autoId: false } });
Person.embedsMany(Address);
db.automigrate(done);
});
it('should create embedded items on scope', function(done) {
Person.create({ name: 'Fred' }, function(err, p) {
p.addressList.create({ id: 'home', street: 'Street 1' }, function(err, addresses) {
p.addressList.create({ id: 'home', street: 'Street 1' }, function(err, address) {
should.not.exist(err);
p.addressList.create({ id: 'work', street: 'Work Street 2' }, function(err, address) {
should.not.exist(err);
@ -1968,6 +2170,17 @@ describe('relations', function () {
});
});
it('should create embedded items with auto-generated id', function(done) {
Person.create({ name: 'Wilma' }, function(err, p) {
p.addressList.create({ street: 'Home Street 1' }, function(err, address) {
should.not.exist(err);
address.id.should.match(/^[0-9a-fA-F]{24}$/);
address.street.should.equal('Home Street 1');
done();
});
});
});
});
describe('embedsMany - relations, scope and properties', function () {
@ -2208,11 +2421,16 @@ describe('relations', function () {
before(function (done) {
db = getSchema();
tmp = getTransientDataSource();
Book = db.define('Book', {name: String});
Author = db.define('Author', {name: String});
Reader = db.define('Reader', {name: String});
Link = db.define('Link', {name: String, notes: String}); // generic model
Link = tmp.define('Link', {
id: {type: Number, id: true},
name: String, notes: String
}); // generic model
Link.validatesPresenceOf('linkedId');
Link.validatesPresenceOf('linkedType');
@ -2226,13 +2444,15 @@ describe('relations', function () {
});
it('can be declared', function (done) {
var idType = db.connector.getDefaultIdType();
Book.embedsMany(Link, { as: 'people',
polymorphic: 'linked',
scope: { include: 'linked' }
});
Link.belongsTo('linked', {
polymorphic: true, // needs unique auto-id
properties: { name: 'name' }, // denormalized
polymorphic: { idType: idType }, // native type
properties: { name: 'name' }, // denormalized
options: { invertProperties: true }
});
db.automigrate(done);
@ -2411,7 +2631,7 @@ describe('relations', function () {
err.name.should.equal('ValidationError');
err.details.codes.jobs.should.eql(['uniqueness']);
var expected = 'The `Category` instance is not valid. ';
expected += 'Details: `jobs` Contains duplicate `Job` instance.';
expected += 'Details: `jobs` contains duplicate `Job` instance.';
err.message.should.equal(expected);
done();
});

View File

@ -229,3 +229,90 @@ describe('scope - filtered count and destroyAll', function () {
});
});
describe('scope - dynamic target class', function () {
var Collection, Media, Image, Video;
before(function () {
db = getSchema();
Image = db.define('Image', {name: String});
Video = db.define('Video', {name: String});
Collection = db.define('Collection', {name: String, modelName: String});
Collection.scope('items', function() {
return {}; // could return a scope based on `this` (receiver)
}, null, {}, { isStatic: false, modelTo: function(receiver) {
return db.models[receiver.modelName];
} });
});
beforeEach(function (done) {
Collection.destroyAll(function() {
Image.destroyAll(function() {
Video.destroyAll(done);
})
});
});
beforeEach(function (done) {
Collection.create({ name: 'Images', modelName: 'Image' }, done);
});
beforeEach(function (done) {
Collection.create({ name: 'Videos', modelName: 'Video' }, done);
});
beforeEach(function (done) {
Collection.create({ name: 'Things', modelName: 'Unknown' }, done);
});
beforeEach(function (done) {
Image.create({ name: 'Image A' }, done);
});
beforeEach(function (done) {
Video.create({ name: 'Video A' }, done);
});
it('should deduce modelTo at runtime - Image', function(done) {
Collection.findOne({ where: { modelName: 'Image' } }, function(err, coll) {
should.not.exist(err);
coll.name.should.equal('Images');
coll.items(function(err, items) {
should.not.exist(err);
items.length.should.equal(1);
items[0].name.should.equal('Image A');
items[0].should.be.instanceof(Image);
done();
});
});
});
it('should deduce modelTo at runtime - Video', function(done) {
Collection.findOne({ where: { modelName: 'Video' } }, function(err, coll) {
should.not.exist(err);
coll.name.should.equal('Videos');
coll.items(function(err, items) {
should.not.exist(err);
items.length.should.equal(1);
items[0].name.should.equal('Video A');
items[0].should.be.instanceof(Video);
done();
});
});
});
it('should throw if modelTo is invalid', function(done) {
Collection.findOne({ where: { name: 'Things' } }, function(err, coll) {
should.not.exist(err);
coll.modelName.should.equal('Unknown');
(function () {
coll.items(function(err, items) {});
}).should.throw();
done();
});
});
});

81
test/transient.test.js Normal file
View File

@ -0,0 +1,81 @@
var jdb = require('../');
var DataSource = jdb.DataSource;
var assert = require('assert');
var async = require('async');
var should = require('./init.js');
var db, TransientModel, Person, Widget, Item;
var getTransientDataSource = function(settings) {
return new DataSource('transient', settings);
};
describe('Transient connector', function () {
before(function () {
db = getTransientDataSource();
TransientModel = db.define('TransientModel', {}, { idInjection: false });
Person = TransientModel.extend('Person', {name: String});
Person.attachTo(db);
Widget = db.define('Widget', {name: String});
Item = db.define('Item', {
id: {type: Number, id: true}, name: String
});
});
it('should respect idInjection being false', function(done) {
should.not.exist(Person.definition.properties.id);
should.exist(Person.definition.properties.name);
Person.create({ name: 'Wilma' }, function(err, inst) {
should.not.exist(err);
inst.toObject().should.eql({ name: 'Wilma' });
Person.count(function(err, count) {
should.not.exist(err);
count.should.equal(0);
done();
});
});
});
it('should generate a random string id', function(done) {
should.exist(Widget.definition.properties.id);
should.exist(Widget.definition.properties.name);
Widget.definition.properties.id.type.should.equal(String);
Widget.create({ name: 'Thing' }, function(err, inst) {
should.not.exist(err);
inst.id.should.match(/^[0-9a-fA-F]{24}$/);
inst.name.should.equal('Thing');
Widget.findById(inst.id, function(err, widget) {
should.not.exist(err);
should.not.exist(widget);
done();
});
});
});
it('should generate a random number id', function(done) {
should.exist(Item.definition.properties.id);
should.exist(Item.definition.properties.name);
Item.definition.properties.id.type.should.equal(Number);
Item.create({ name: 'Example' }, function(err, inst) {
should.not.exist(err);
inst.name.should.equal('Example');
Item.count(function(err, count) {
should.not.exist(err);
count.should.equal(0);
done();
});
});
});
});

View File

@ -39,7 +39,7 @@ describe('validations', function () {
beforeEach(function (done) {
User.destroyAll(function () {
delete User._validations;
delete User.validations;
done();
});
});
@ -67,7 +67,7 @@ describe('validations', function () {
describe('lifecycle', function () {
it('should work on create', function (done) {
delete User._validations;
delete User.validations;
User.validatesPresenceOf('name');
User.create(function (e, u) {
should.exist(e);
@ -79,7 +79,7 @@ describe('validations', function () {
});
it('should work on update', function (done) {
delete User._validations;
delete User.validations;
User.validatesPresenceOf('name');
User.create({name: 'Valid'}, function (e, d) {
d.updateAttribute('name', null, function (e) {
@ -95,7 +95,7 @@ describe('validations', function () {
});
it('should return error code', function (done) {
delete User._validations;
delete User.validations;
User.validatesPresenceOf('name');
User.create(function (e, u) {
should.exist(e);
@ -112,7 +112,7 @@ describe('validations', function () {
});
it('should include validation messages in err.message', function(done) {
delete User._validations;
delete User.validations;
User.validatesPresenceOf('name');
User.create(function (e, u) {
should.exist(e);
@ -122,7 +122,7 @@ describe('validations', function () {
});
it('should include model name in err.message', function(done) {
delete User._validations;
delete User.validations;
User.validatesPresenceOf('name');
User.create(function (e, u) {
should.exist(e);
@ -130,6 +130,14 @@ describe('validations', function () {
done();
});
});
it('should return validation metadata', function() {
var expected = {name:[{validation: 'presence', options: {}}]};
delete User.validations;
User.validatesPresenceOf('name');
var validations = User.validations;
validations.should.eql(expected);
});
});
});