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'); 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 {DataSource} dataSource The loopback-datasource-juggler dataSource
* @param {Function} [callback] The callback function * @param {Function} [callback] The callback function
@ -406,9 +406,10 @@ function applyFilter(filter) {
if (typeof value === 'string' && (example instanceof RegExp)) { if (typeof value === 'string' && (example instanceof RegExp)) {
return value.match(example); return value.match(example);
} }
if (example === undefined || value === undefined) { if (example === undefined) {
return undefined; return undefined;
} }
if (typeof example === 'object') { if (typeof example === 'object') {
// ignore geo near filter // ignore geo near filter
if (example.near) { if (example.near) {
@ -425,6 +426,10 @@ function applyFilter(filter) {
return false; return false;
} }
if ('neq' in example) {
return compare(example.neq, value) !== 0;
}
if (example.like || example.nlike) { if (example.like || example.nlike) {
var like = example.like || example.nlike; var like = example.like || example.nlike;
@ -445,7 +450,8 @@ function applyFilter(filter) {
} }
} }
// not strict equality // 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 * Module dependencies
*/ */
var jutil = require('./jutil'); var jutil = require('./jutil');
var validations = require('./validations.js'); var ValidationError = require('./validations').ValidationError;
var ValidationError = validations.ValidationError;
var Relation = require('./relations.js'); var Relation = require('./relations.js');
var Inclusion = require('./include.js'); var Inclusion = require('./include.js');
var List = require('./list.js'); var List = require('./list.js');
@ -40,15 +39,12 @@ function DataAccessObject() {
} }
} }
function idName(m) { function idName(m) {
return m.getDataSource().idName return m.definition.idName() || 'id';
? m.getDataSource().idName(m.modelName) : 'id';
} }
function getIdValue(m, data) { function getIdValue(m, data) {
return data && data[m.getDataSource().idName(m.modelName)]; return data && data[idName(m)];
} }
function setIdValue(m, data, value) { function setIdValue(m, data, value) {
@ -331,7 +327,7 @@ DataAccessObject.findByIds = function(ids, cond, cb) {
cond = {}; cond = {};
} }
var pk = this.dataSource.idName(this.modelName) || 'id'; var pk = idName(this);
if (ids.length === 0) { if (ids.length === 0) {
process.nextTick(function() { cb(null, []); }); process.nextTick(function() { cb(null, []); });
return; return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,10 +12,17 @@ module.exports = require('should');
} }
*/ */
var ModelBuilder = require('../').ModelBuilder;
var Schema = require('../').Schema; var Schema = require('../').Schema;
if (!('getSchema' in global)) { if (!('getSchema' in global)) {
global.getSchema = function () { global.getSchema = function (connector, settings) {
return new Schema('memory'); 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(); 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) { it('should set up relations after attach', function (done) {
var ds = new DataSource('memory'); var ds = new DataSource('memory');
var modelBuilder = new ModelBuilder(); var modelBuilder = new ModelBuilder();

View File

@ -102,7 +102,7 @@ describe('manipulation', function () {
Person.validatesPresenceOf('name'); Person.validatesPresenceOf('name');
Person.create(batch,function (errors, persons) { Person.create(batch,function (errors, persons) {
delete Person._validations; delete Person.validations;
should.exist(errors); should.exist(errors);
errors.should.have.lengthOf(batch.length); errors.should.have.lengthOf(batch.length);
should.not.exist(errors[0]); 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) { function seed(done) {
var beatles = [ var beatles = [
{ {

View File

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

View File

@ -1,12 +1,18 @@
// This test written in mocha+should.js // This test written in mocha+should.js
var should = require('./init.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 Category, Job;
var Picture, PictureLink; var Picture, PictureLink;
var Person, Address; var Person, Address;
var Link; var Link;
var getTransientDataSource = function(settings) {
return new DataSource('transient', settings, db.modelBuilder);
};
describe('relations', function () { describe('relations', function () {
describe('hasMany', 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) { it('should allow to remove connection with instance', function (done) {
var id; var id;
Physician.create(function (err, physician) { 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 () { describe('hasMany with properties', function () {
it('can be declared with properties', function (done) { it('can be declared with properties', function (done) {
Book.hasMany(Chapter, { properties: { type: 'bookType' } }); Book.hasMany(Chapter, { properties: { type: 'bookType' } });
@ -915,6 +1091,29 @@ describe('relations', function () {
db.automigrate(done); 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 = []; var author, reader, pictures = [];
it('should create polymorphic relation - author', function (done) { it('should create polymorphic relation - author', function (done) {
Author.create({ name: 'Author 1' }, function (err, a) { Author.create({ name: 'Author 1' }, function (err, a) {
@ -1486,9 +1685,10 @@ describe('relations', function () {
var Other; var Other;
before(function () { before(function () {
tmp = getTransientDataSource();
db = getSchema(); db = getSchema();
Person = db.define('Person', {name: String}); Person = db.define('Person', {name: String});
Passport = db.define('Passport', Passport = tmp.define('Passport',
{name:{type:'string', required: true}}, {name:{type:'string', required: true}},
{idInjection: false} {idInjection: false}
); );
@ -1634,9 +1834,10 @@ describe('relations', function () {
var address1, address2; var address1, address2;
before(function (done) { before(function (done) {
tmp = getTransientDataSource({defaultIdType: Number});
db = getSchema(); db = getSchema();
Person = db.define('Person', {name: String}); Person = db.define('Person', {name: String});
Address = db.define('Address', {street: String}); Address = tmp.define('Address', {street: String});
Address.validatesPresenceOf('street'); Address.validatesPresenceOf('street');
db.automigrate(function () { db.automigrate(function () {
@ -1813,9 +2014,10 @@ describe('relations', function () {
describe('embedsMany - explicit ids', function () { describe('embedsMany - explicit ids', function () {
before(function (done) { before(function (done) {
tmp = getTransientDataSource();
db = getSchema(); db = getSchema();
Person = db.define('Person', {name: String}); 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'); Address.validatesPresenceOf('street');
db.automigrate(function () { db.automigrate(function () {
@ -1824,13 +2026,13 @@ describe('relations', function () {
}); });
it('can be declared', function (done) { it('can be declared', function (done) {
Person.embedsMany(Address, { options: { autoId: false } }); Person.embedsMany(Address);
db.automigrate(done); db.automigrate(done);
}); });
it('should create embedded items on scope', function(done) { it('should create embedded items on scope', function(done) {
Person.create({ name: 'Fred' }, function(err, p) { 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); should.not.exist(err);
p.addressList.create({ id: 'work', street: 'Work Street 2' }, function(err, address) { p.addressList.create({ id: 'work', street: 'Work Street 2' }, function(err, address) {
should.not.exist(err); 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 () { describe('embedsMany - relations, scope and properties', function () {
@ -2208,11 +2421,16 @@ describe('relations', function () {
before(function (done) { before(function (done) {
db = getSchema(); db = getSchema();
tmp = getTransientDataSource();
Book = db.define('Book', {name: String}); Book = db.define('Book', {name: String});
Author = db.define('Author', {name: String}); Author = db.define('Author', {name: String});
Reader = db.define('Reader', {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('linkedId');
Link.validatesPresenceOf('linkedType'); Link.validatesPresenceOf('linkedType');
@ -2226,13 +2444,15 @@ describe('relations', function () {
}); });
it('can be declared', function (done) { it('can be declared', function (done) {
var idType = db.connector.getDefaultIdType();
Book.embedsMany(Link, { as: 'people', Book.embedsMany(Link, { as: 'people',
polymorphic: 'linked', polymorphic: 'linked',
scope: { include: 'linked' } scope: { include: 'linked' }
}); });
Link.belongsTo('linked', { Link.belongsTo('linked', {
polymorphic: true, // needs unique auto-id polymorphic: { idType: idType }, // native type
properties: { name: 'name' }, // denormalized properties: { name: 'name' }, // denormalized
options: { invertProperties: true } options: { invertProperties: true }
}); });
db.automigrate(done); db.automigrate(done);
@ -2411,7 +2631,7 @@ describe('relations', function () {
err.name.should.equal('ValidationError'); err.name.should.equal('ValidationError');
err.details.codes.jobs.should.eql(['uniqueness']); err.details.codes.jobs.should.eql(['uniqueness']);
var expected = 'The `Category` instance is not valid. '; 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); err.message.should.equal(expected);
done(); 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) { beforeEach(function (done) {
User.destroyAll(function () { User.destroyAll(function () {
delete User._validations; delete User.validations;
done(); done();
}); });
}); });
@ -67,7 +67,7 @@ describe('validations', function () {
describe('lifecycle', function () { describe('lifecycle', function () {
it('should work on create', function (done) { it('should work on create', function (done) {
delete User._validations; delete User.validations;
User.validatesPresenceOf('name'); User.validatesPresenceOf('name');
User.create(function (e, u) { User.create(function (e, u) {
should.exist(e); should.exist(e);
@ -79,7 +79,7 @@ describe('validations', function () {
}); });
it('should work on update', function (done) { it('should work on update', function (done) {
delete User._validations; delete User.validations;
User.validatesPresenceOf('name'); User.validatesPresenceOf('name');
User.create({name: 'Valid'}, function (e, d) { User.create({name: 'Valid'}, function (e, d) {
d.updateAttribute('name', null, function (e) { d.updateAttribute('name', null, function (e) {
@ -95,7 +95,7 @@ describe('validations', function () {
}); });
it('should return error code', function (done) { it('should return error code', function (done) {
delete User._validations; delete User.validations;
User.validatesPresenceOf('name'); User.validatesPresenceOf('name');
User.create(function (e, u) { User.create(function (e, u) {
should.exist(e); should.exist(e);
@ -112,7 +112,7 @@ describe('validations', function () {
}); });
it('should include validation messages in err.message', function(done) { it('should include validation messages in err.message', function(done) {
delete User._validations; delete User.validations;
User.validatesPresenceOf('name'); User.validatesPresenceOf('name');
User.create(function (e, u) { User.create(function (e, u) {
should.exist(e); should.exist(e);
@ -122,7 +122,7 @@ describe('validations', function () {
}); });
it('should include model name in err.message', function(done) { it('should include model name in err.message', function(done) {
delete User._validations; delete User.validations;
User.validatesPresenceOf('name'); User.validatesPresenceOf('name');
User.create(function (e, u) { User.create(function (e, u) {
should.exist(e); should.exist(e);
@ -130,6 +130,14 @@ describe('validations', function () {
done(); 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);
});
}); });
}); });