diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 97687cf2..ea9c9d3a 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -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); } /** diff --git a/lib/connectors/transient.js b/lib/connectors/transient.js new file mode 100644 index 00000000..ce8354ce --- /dev/null +++ b/lib/connectors/transient.js @@ -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); +}; diff --git a/lib/dao.js b/lib/dao.js index 58ca673f..6806209a 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -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; diff --git a/lib/datasource.js b/lib/datasource.js index c55ed685..19522e29 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -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); } - } + }); } }; diff --git a/lib/model-definition.js b/lib/model-definition.js index ae53a1be..32bbcd1c 100644 --- a/lib/model-definition.js +++ b/lib/model-definition.js @@ -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; diff --git a/lib/model.js b/lib/model.js index 069d39c1..5d29a5d8 100644 --- a/lib/model.js +++ b/lib/model.js @@ -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 diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 29c48f13..3a5b7371 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -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); } diff --git a/lib/scope.js b/lib/scope.js index 0dd2b40b..5124251d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -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; diff --git a/lib/validations.js b/lib/validations.js index 9c67a6af..62d91dbc 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -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() { diff --git a/package.json b/package.json index c2d3588f..1cf272e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "2.7.0", + "version": "2.8.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", diff --git a/test/init.js b/test/init.js index 948ccecd..200cabd0 100644 --- a/test/init.js +++ b/test/init.js @@ -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(); + }; +} \ No newline at end of file diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 940c9a79..bb02bfcf 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -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(); diff --git a/test/manipulation.test.js b/test/manipulation.test.js index d31b3b9d..2d609063 100644 --- a/test/manipulation.test.js +++ b/test/manipulation.test.js @@ -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]); diff --git a/test/memory.test.js b/test/memory.test.js index 40283b82..453ac9e8 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -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 = [ { diff --git a/test/model-definition.test.js b/test/model-definition.test.js index 080685e4..e7f22ff1 100644 --- a/test/model-definition.test.js +++ b/test/model-definition.test.js @@ -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(); }); diff --git a/test/relations.test.js b/test/relations.test.js index ef212363..b99f31ba 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -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(); }); diff --git a/test/scope.test.js b/test/scope.test.js index cb464ce4..b708870d 100644 --- a/test/scope.test.js +++ b/test/scope.test.js @@ -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(); + }); + }); + +}); diff --git a/test/transient.test.js b/test/transient.test.js new file mode 100644 index 00000000..e3a03f96 --- /dev/null +++ b/test/transient.test.js @@ -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(); + }); + }); + }); + +}); diff --git a/test/validations.test.js b/test/validations.test.js index 935a2669..97ee49fe 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -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); + }); }); });