diff --git a/lib/dao.js b/lib/dao.js index 3d76be09..58ca673f 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -338,7 +338,7 @@ DataAccessObject.findByIds = function(ids, cond, cb) { } var filter = { where: {} }; - filter.where[pk] = { inq: ids }; + filter.where[pk] = { inq: [].concat(ids) }; mergeQuery(filter, cond || {}); this.find(filter, function(err, results) { cb(err, err ? results : utils.sortObjectsByIds(pk, ids, results)); @@ -1113,6 +1113,15 @@ DataAccessObject.prototype.setAttributes = function setAttributes(data) { Model.emit('set', inst); }; +DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) { + if (nullify) { + this[name] = this.__data[name] = null; + } else { + delete this[name]; + delete this.__data[name]; + } +}; + /** * Update set of attributes. * Performs validation before updating. diff --git a/lib/include.js b/lib/include.js index 5fe6548c..dacf78af 100644 --- a/lib/include.js +++ b/lib/include.js @@ -42,7 +42,7 @@ function Inclusion() { */ Inclusion.include = function (objects, include, cb) { var self = this; - + if (!include || (Array.isArray(include) && include.length === 0) || (isPlainObject(include) && Object.keys(include).length === 0)) { // The objects are empty @@ -123,6 +123,7 @@ Inclusion.include = function (objects, include, cb) { return callback(); } } + var inst = (obj instanceof self) ? obj : new self(obj); // Calling the relation method on the instance inst[relationName](function (err, result) { @@ -131,6 +132,7 @@ Inclusion.include = function (objects, include, cb) { } else { defineCachedRelations(obj); obj.__cachedRelations[relationName] = result; + if(obj === inst) { obj.__data[relationName] = result; obj.setStrict(false); diff --git a/lib/model.js b/lib/model.js index b1c28a99..ee60ce72 100644 --- a/lib/model.js +++ b/lib/model.js @@ -12,6 +12,7 @@ var jutil = require('./jutil'); var List = require('./list'); var Hookable = require('./hooks'); var validations = require('./validations.js'); +var _extend = util._extend; // Set up an object for quick lookup var BASE_TYPES = { @@ -56,7 +57,7 @@ ModelBaseClass.prototype._initProperties = function (data, options) { // Convert the data to be plain object to avoid pollutions data = data.toObject(false); } - var properties = ctor.definition.properties; + var properties = _extend({}, ctor.definition.properties); data = data || {}; options = options || {}; @@ -130,27 +131,39 @@ ModelBaseClass.prototype._initProperties = function (data, options) { self.__data[p] = propVal; } } else if (ctor.relations[p]) { + if (!properties[p]) { + var modelTo = ctor.relations[p].modelTo || ModelBaseClass; + var multiple = ctor.relations[p].multiple; + var typeName = multiple ? 'Array' : modelTo.modelName; + var propType = multiple ? [modelTo] : modelTo; + properties[p] = { name: typeName, type: propType }; + this.setStrict(false); + } + // Relation if (ctor.relations[p].type === 'belongsTo' && propVal != null) { // If the related model is populated self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo]; + } else if (!self.__data[p] && propVal != null) { + self.__data[p] = propVal; } self.__cachedRelations[p] = propVal; } else { // Un-managed property - if (strict === false) { - self[p] = self.__data[p] = propVal; + if (strict === false || self.__cachedRelations[p]) { + self[p] = self.__data[p] = propVal || self.__cachedRelations[p]; } else if (strict === 'throw') { throw new Error('Unknown property: ' + p); } } } - + keys = Object.keys(properties); size = keys.length; for (k = 0; k < size; k++) { p = keys[k]; + // var prop propVal = self.__data[p]; // Set default values @@ -172,10 +185,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) { self.__data[p] = def; } } - + // Handle complex types (JSON/Object) var type = properties[p].type; - if (! BASE_TYPES[type.name]) { + if (!BASE_TYPES[type.name]) { + if (typeof self.__data[p] !== 'object' && self.__data[p]) { try { self.__data[p] = JSON.parse(self.__data[p] + ''); @@ -183,7 +197,14 @@ ModelBaseClass.prototype._initProperties = function (data, options) { self.__data[p] = String(self.__data[p]); } } - if (type.name === 'Array' || Array.isArray(type)) { + + if (type.prototype instanceof ModelBaseClass) { + if (!(self.__data[p] instanceof type) + && typeof self.__data[p] === 'object' + && self.__data[p] !== null ) { + self.__data[p] = new type(self.__data[p]); + } + } else if (type.name === 'Array' || Array.isArray(type)) { if (!(self.__data[p] instanceof List) && self.__data[p] !== undefined && self.__data[p] !== null ) { diff --git a/lib/relation-definition.js b/lib/relation-definition.js index 84ebc44e..b224faad 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -20,6 +20,7 @@ var RelationTypes = { hasOne: 'hasOne', hasAndBelongsToMany: 'hasAndBelongsToMany', referencesMany: 'referencesMany', + embedsOne: 'embedsOne', embedsMany: 'embedsMany' }; @@ -30,6 +31,7 @@ exports.HasOne = HasOne; exports.HasAndBelongsToMany = HasAndBelongsToMany; exports.BelongsTo = BelongsTo; exports.ReferencesMany = ReferencesMany; +exports.EmbedsOne = EmbedsOne; exports.EmbedsMany = EmbedsMany; var RelationClasses = { @@ -39,6 +41,7 @@ var RelationClasses = { hasOne: HasOne, hasAndBelongsToMany: HasAndBelongsToMany, referencesMany: ReferencesMany, + embedsOne: EmbedsOne, embedsMany: EmbedsMany }; @@ -384,6 +387,24 @@ function HasOne(definition, modelInstance) { util.inherits(HasOne, Relation); +/** + * EmbedsOne subclass + * @param {RelationDefinition|Object} definition + * @param {Object} modelInstance + * @returns {EmbedsMany} + * @constructor + * @class EmbedsOne + */ +function EmbedsOne(definition, modelInstance) { + if (!(this instanceof EmbedsOne)) { + return new EmbedsMany(definition, modelInstance); + } + assert(definition.type === RelationTypes.embedsOne); + Relation.apply(this, arguments); +} + +util.inherits(EmbedsOne, Relation); + /** * EmbedsMany subclass * @param {RelationDefinition|Object} definition @@ -954,7 +975,7 @@ HasManyThrough.prototype.exists = function (acInst, done) { var keys = throughKeys(definition); var fk1 = keys[0]; var fk2 = keys[1]; - + query[fk1] = this.modelInstance[pk1]; query[fk2] = (acInst instanceof definition.modelTo) ? acInst[pk2] : acInst; @@ -1084,9 +1105,11 @@ RelationDefinition.belongsTo = function (modelFrom, modelTo, params) { get: function() { var relation = new BelongsTo(definition, this); var relationMethod = relation.related.bind(relation); - relationMethod.create = relation.create.bind(relation); - relationMethod.build = relation.build.bind(relation); - if (definition.modelTo) { + relationMethod.update = relation.update.bind(relation); + relationMethod.destroy = relation.destroy.bind(relation); + if (!polymorphic) { + relationMethod.create = relation.create.bind(relation); + relationMethod.build = relation.build.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; } return relationMethod; @@ -1139,6 +1162,36 @@ BelongsTo.prototype.build = function(targetModelData) { return new modelTo(targetModelData); }; +BelongsTo.prototype.update = function (targetModelData, cb) { + var definition = this.definition; + this.fetch(function(err, inst) { + if (inst instanceof ModelBaseClass) { + inst.updateAttributes(targetModelData, cb); + } else { + cb(new Error('BelongsTo relation ' + definition.name + + ' is empty')); + } + }); +}; + +BelongsTo.prototype.destroy = function (cb) { + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + var fk = this.definition.keyFrom; + this.fetch(function(err, targetModel) { + if (targetModel instanceof ModelBaseClass) { + modelInstance[fk] = null; + modelInstance.save(function(err, targetModel) { + if (cb && err) return cb && cb(err); + cb && cb(err, targetModel); + }); + } else { + cb(new Error('BelongsTo relation ' + definition.name + + ' is empty')); + } + }); +}; + /** * Define the method for the belongsTo relation itself * It will support one of the following styles: @@ -1175,7 +1228,6 @@ BelongsTo.prototype.related = function (refresh, params) { cachedValue = self.getCache(); } if (params instanceof ModelBaseClass) { // acts as setter - modelTo = params.constructor; modelInstance[fk] = params[pk]; if (discriminator) { @@ -1204,7 +1256,8 @@ BelongsTo.prototype.related = function (refresh, params) { var query = {where: {}}; query.where[pk] = modelInstance[fk]; - if (query.where[pk] === undefined) { + if (query.where[pk] === undefined + || query.where[pk] === null) { // Foreign key is undefined return process.nextTick(cb); } @@ -1353,6 +1406,8 @@ RelationDefinition.hasOne = function (modelFrom, modelTo, params) { var relationMethod = relation.related.bind(relation) relationMethod.create = relation.create.bind(relation); relationMethod.build = relation.build.bind(relation); + relationMethod.update = relation.update.bind(relation); + relationMethod.destroy = relation.destroy.bind(relation); relationMethod._targetClass = definition.modelTo.modelName; return relationMethod; } @@ -1416,6 +1471,29 @@ HasOne.prototype.create = function (targetModelData, cb) { }); }; +HasOne.prototype.update = function (targetModelData, cb) { + var definition = this.definition; + this.fetch(function(err, targetModel) { + if (targetModel instanceof ModelBaseClass) { + targetModel.updateAttributes(targetModelData, cb); + } else { + cb(new Error('HasOne relation ' + definition.name + + ' is empty')); + } + }); +}; + +HasOne.prototype.destroy = function (cb) { + this.fetch(function(err, targetModel) { + if (targetModel instanceof ModelBaseClass) { + targetModel.destroy(cb); + } else { + cb(new Error('HasOne relation ' + definition.name + + ' is empty')); + } + }); +}; + /** * Create a target model instance * @param {Object} targetModelData The target model data @@ -1539,6 +1617,198 @@ HasOne.prototype.related = function (refresh, params) { } }; +RelationDefinition.embedsOne = function (modelFrom, modelTo, params) { + params = params || {}; + modelTo = lookupModelTo(modelFrom, modelTo, params); + + var thisClassName = modelFrom.modelName; + var relationName = params.as || (i8n.camelize(modelTo.modelName, true) + 'Item'); + var propertyName = params.property || i8n.camelize(modelTo.modelName, true); + var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id'; + + if (relationName === propertyName) { + propertyName = '_' + propertyName; + debug('EmbedsOne property cannot be equal to relation name: ' + + 'forcing property %s for relation %s', propertyName, relationName); + } + + var definition = modelFrom.relations[relationName] = new RelationDefinition({ + name: relationName, + type: RelationTypes.embedsOne, + modelFrom: modelFrom, + keyFrom: propertyName, + keyTo: idName, + modelTo: modelTo, + multiple: false, + properties: params.properties, + scope: params.scope, + options: params.options, + embed: true + }); + + var opts = { type: modelTo }; + + if (params.default === true) { + opts.default = function() { return new modelTo(); }; + } else if (typeof params.default === 'object') { + opts.default = (function(def) { + return function() { + return new modelTo(def); + }; + }(params.default)); + } + + modelFrom.dataSource.defineProperty(modelFrom.modelName, propertyName, opts); + + // validate the embedded instance + if (definition.options.validate !== false) { + modelFrom.validate(relationName, function(err) { + var inst = this[propertyName]; + if (inst instanceof modelTo) { + if (!inst.isValid()) { + var first = Object.keys(inst.errors)[0]; + var msg = 'is invalid: `' + first + '` ' + inst.errors[first]; + this.errors.add(relationName, msg, 'invalid'); + err(false); + } + } + }); + } + + // Define a property for the scope so that we have 'this' for the scoped methods + Object.defineProperty(modelFrom.prototype, relationName, { + enumerable: true, + configurable: true, + get: function() { + var relation = new EmbedsOne(definition, this); + var relationMethod = relation.related.bind(relation) + relationMethod.create = relation.create.bind(relation); + relationMethod.build = relation.build.bind(relation); + relationMethod.update = relation.update.bind(relation); + relationMethod.destroy = relation.destroy.bind(relation); + relationMethod._targetClass = definition.modelTo.modelName; + return relationMethod; + } + }); + + // FIXME: [rfeng] Wrap the property into a function for remoting + // so that it can be accessed as /api/// + // For example, /api/orders/1/customer + var fn = function() { + var f = this[relationName]; + f.apply(this, arguments); + }; + modelFrom.prototype['__get__' + relationName] = fn; + + return definition; +}; + +EmbedsOne.prototype.related = function (refresh, params) { + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + var propertyName = this.definition.keyFrom; + + if (arguments.length === 1) { + params = refresh; + refresh = false; + } else if (arguments.length > 2) { + throw new Error('Method can\'t be called with more than two arguments'); + } + + if (params instanceof ModelBaseClass) { // acts as setter + if (params instanceof modelTo) { + this.definition.applyProperties(modelInstance, params); + modelInstance.setAttribute(propertyName, params); + } + return; + } + + var embeddedInstance = modelInstance[propertyName]; + if (typeof params === 'function') { // acts as async getter + var cb = params; + process.nextTick(function() { + cb(null, embeddedInstance); + }); + } else if (params === undefined) { // acts as sync getter + return embeddedInstance; + } +}; + +EmbedsOne.prototype.create = function (targetModelData, cb) { + var modelTo = this.definition.modelTo; + var propertyName = this.definition.keyFrom; + var modelInstance = this.modelInstance; + + if (typeof targetModelData === 'function' && !cb) { + cb = targetModelData; + targetModelData = {}; + } + + targetModelData = targetModelData || {}; + + var inst = this.build(targetModelData); + + var err = inst.isValid() ? null : new ValidationError(inst); + + if (err) { + return process.nextTick(function() { + cb(err); + }); + } + + modelInstance.updateAttribute(propertyName, + inst, function(err) { + cb(err, err ? null : inst); + }); +}; + +EmbedsOne.prototype.build = function (targetModelData) { + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + var propertyName = this.definition.keyFrom; + + targetModelData = targetModelData || {}; + + this.definition.applyProperties(modelInstance, targetModelData); + + var embeddedInstance = new modelTo(targetModelData); + modelInstance[propertyName] = embeddedInstance; + + return embeddedInstance; +}; + +EmbedsOne.prototype.update = function (targetModelData, cb) { + var modelTo = this.definition.modelTo; + var modelInstance = this.modelInstance; + var propertyName = this.definition.keyFrom; + + var isInst = targetModelData instanceof ModelBaseClass; + var data = isInst ? targetModelData.toObject() : targetModelData; + + var embeddedInstance = modelInstance[propertyName]; + if (embeddedInstance instanceof modelTo) { + embeddedInstance.setAttributes(data); + if (typeof cb === 'function') { + modelInstance.save(function(err, inst) { + cb(err, inst ? inst[propertyName] : embeddedInstance); + }); + } + } else if (!embeddedInstance && cb) { + this.create(data, db); + } else if (!embeddedInstance) { + this.build(data); + } +}; + +EmbedsOne.prototype.destroy = function (cb) { + var modelInstance = this.modelInstance; + var propertyName = this.definition.keyFrom; + modelInstance.unsetAttribute(propertyName, true); + modelInstance.save(function (err, result) { + cb && cb(err, result); + }); +}; + RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) { params = params || {}; modelTo = lookupModelTo(modelFrom, modelTo, params, true); @@ -1590,7 +1860,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) } // validate all embedded items - if (definition.options.validate) { + if (definition.options.validate !== false) { modelFrom.validate(propertyName, function(err) { var self = this; var embeddedList = this[propertyName] || []; @@ -1602,7 +1872,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) var id = item[idName] || '(blank)'; var first = Object.keys(item.errors)[0]; var msg = 'contains invalid item: `' + id + '`'; - msg += ' (' + first + ' ' + item.errors[first] + ')'; + msg += ' (`' + first + '` ' + item.errors[first] + ')'; self.errors.add(propertyName, msg, 'invalid'); } } else { @@ -1716,15 +1986,11 @@ EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb var modelInstance = this.modelInstance; var actualCond = {}; - var actualRefresh = false; if (arguments.length === 3) { cb = condOrRefresh; } else if (arguments.length === 4) { - if (typeof condOrRefresh === 'boolean') { - actualRefresh = condOrRefresh; - } else { + if (typeof condOrRefresh === 'object') { actualCond = condOrRefresh; - actualRefresh = true; } } else { throw new Error('Method can be only called with one or two arguments'); @@ -1923,7 +2189,7 @@ EmbedsMany.prototype.build = function(targetModelData) { } } - this.definition.applyProperties(this.modelInstance, targetModelData); + this.definition.applyProperties(modelInstance, targetModelData); var inst = new modelTo(targetModelData); diff --git a/lib/relations.js b/lib/relations.js index ba9a36d7..790433fc 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -158,14 +158,18 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params return RelationDefinition.hasAndBelongsToMany(this, modelTo, params); }; -RelationMixin.hasOne = function hasMany(modelTo, params) { +RelationMixin.hasOne = function hasOne(modelTo, params) { return RelationDefinition.hasOne(this, modelTo, params); }; -RelationMixin.referencesMany = function hasMany(modelTo, params) { +RelationMixin.referencesMany = function referencesMany(modelTo, params) { return RelationDefinition.referencesMany(this, modelTo, params); }; -RelationMixin.embedsMany = function hasMany(modelTo, params) { +RelationMixin.embedsOne = function embedsOne(modelTo, params) { + return RelationDefinition.embedsOne(this, modelTo, params); +}; + +RelationMixin.embedsMany = function embedsMany(modelTo, params) { return RelationDefinition.embedsMany(this, modelTo, params); }; diff --git a/lib/utils.js b/lib/utils.js index 6afb9129..c774ecae 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -210,12 +210,12 @@ function isPlainObject(obj) { function sortObjectsByIds(idName, ids, objects, strict) { ids = ids.map(function(id) { - return (typeof id === 'object') ? id.toString() : id; + return (typeof id === 'object') ? String(id) : id; }); var indexOf = function(x) { var isObj = (typeof x[idName] === 'object'); // ObjectID - var id = isObj ? x[idName].toString() : x[idName]; + var id = isObj ? String(x[idName]) : x[idName]; return ids.indexOf(id); }; diff --git a/package.json b/package.json index 7e75fb17..cb4c961c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "2.4.2", + "version": "2.5.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", diff --git a/test/relations.test.js b/test/relations.test.js index 871ecb54..7751ee23 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -189,7 +189,7 @@ describe('relations', function () { }); describe('hasMany through', function () { - var Physician, Patient, Appointment; + var Physician, Patient, Appointment, Address; before(function (done) { db = getSchema(); @@ -199,13 +199,15 @@ describe('relations', function () { default: function () { return new Date(); }}}); - + Address = db.define('Address', {name: String}); + Physician.hasMany(Patient, {through: Appointment}); Patient.hasMany(Physician, {through: Appointment}); + Patient.belongsTo(Address); Appointment.belongsTo(Patient); Appointment.belongsTo(Physician); - db.automigrate(['Physician', 'Patient', 'Appointment'], function (err) { + db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'], function (err) { done(err); }); }); @@ -277,11 +279,10 @@ describe('relations', function () { }); it('should allow to use include syntax on related data', function (done) { - var Address = db.define('Address', {name: String}); - Patient.belongsTo(Address); Physician.create(function (err, physician) { physician.patients.create({name: 'a'}, function (err, patient) { Address.create({name: 'z'}, function (err, address) { + should.not.exist(err); patient.address(address); patient.save(function() { verify(physician, address.id); @@ -353,6 +354,7 @@ describe('relations', function () { var id; Physician.create(function (err, physician) { physician.patients.create({name: 'a'}, function (err, ch) { + should.not.exist(err); id = ch.id; physician.patients.create({name: 'z'}, function () { physician.patients.create({name: 'c'}, function () { @@ -1110,6 +1112,8 @@ describe('relations', function () { describe('belongsTo', function () { var List, Item, Fear, Mind; + + var listId, itemId; it('can be declared in different ways', function () { List = db.define('List', {name: String}); @@ -1132,15 +1136,18 @@ describe('relations', function () { it('can be used to query data', function (done) { List.hasMany('todos', {model: Item}); db.automigrate(function () { - List.create(function (e, list) { + List.create({name: 'List 1'}, function (e, list) { + listId = list.id; should.not.exist(e); should.exist(list); - list.todos.create(function (err, todo) { + list.todos.create({name: 'Item 1'},function (err, todo) { + itemId = todo.id; todo.list(function (e, l) { should.not.exist(e); should.exist(l); l.should.be.an.instanceOf(List); todo.list().id.should.equal(l.id); + todo.list().name.should.equal('List 1'); done(); }); }); @@ -1162,6 +1169,55 @@ describe('relations', function () { }); }); }); + + it('should update related item on scope', function(done) { + Item.findById(itemId, function (e, todo) { + todo.list.update({name: 'List A'}, function(err, list) { + should.not.exist(err); + should.exist(list); + list.name.should.equal('List A'); + done(); + }); + }); + }); + + it('should get related item on scope', function(done) { + Item.findById(itemId, function (e, todo) { + todo.list(function(err, list) { + should.not.exist(err); + should.exist(list); + list.name.should.equal('List A'); + done(); + }); + }); + }); + + it('should destroy related item on scope', function(done) { + Item.findById(itemId, function (e, todo) { + todo.list.destroy(function(err) { + should.not.exist(err); + done(); + }); + }); + }); + + it('should get related item on scope - verify', function(done) { + Item.findById(itemId, function (e, todo) { + todo.list(function(err, list) { + should.not.exist(err); + should.not.exist(list); + done(); + }); + }); + }); + + it('should not have deleted related item', function(done) { + List.findById(listId, function (e, list) { + should.not.exist(e); + should.exist(list); + done(); + }); + }); }); @@ -1169,7 +1225,7 @@ describe('relations', function () { var Person, Passport; it('can be declared with scope and properties', function (done) { - Person = db.define('Person', {name: String, age: Number}); + Person = db.define('Person', {name: String, age: Number, passportNotes: String}); Passport = db.define('Passport', {name: String, notes: String}); Passport.belongsTo(Person, { properties: { notes: 'passportNotes' }, @@ -1206,6 +1262,7 @@ describe('relations', function () { describe('hasOne', function () { var Supplier, Account; + var supplierId, accountId; before(function () { db = getSchema(); @@ -1220,13 +1277,14 @@ describe('relations', function () { }); it('can be used to query data', function (done) { - // Supplier.hasOne(Account); db.automigrate(function () { Supplier.create({name: 'Supplier 1'}, function (e, supplier) { + supplierId = supplier.id; should.not.exist(e); should.exist(supplier); supplier.account.create({accountNo: 'a01'}, function (err, account) { supplier.account(function (e, act) { + accountId = act.id; should.not.exist(e); should.exist(act); act.should.be.an.instanceOf(Account); @@ -1242,6 +1300,63 @@ describe('relations', function () { it('should set targetClass on scope property', function() { should.equal(Supplier.prototype.account._targetClass, 'Account'); }); + + it('should update the related item on scope', function(done) { + Supplier.findById(supplierId, function(e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account.update({supplierName: 'Supplier A'}, function(err, act) { + should.not.exist(e); + act.supplierName.should.equal('Supplier A'); + done(); + }); + }); + }); + + it('should get the related item on scope', function(done) { + Supplier.findById(supplierId, function(e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account(function(err, act) { + should.not.exist(e); + should.exist(act); + act.supplierName.should.equal('Supplier A'); + done(); + }); + }); + }); + + it('should destroy the related item on scope', function(done) { + Supplier.findById(supplierId, function(e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account.destroy(function(err) { + should.not.exist(e); + done(); + }); + }); + }); + + it('should get the related item on scope - verify', function(done) { + Supplier.findById(supplierId, function(e, supplier) { + should.not.exist(e); + should.exist(supplier); + supplier.account(function(err, act) { + should.not.exist(e); + should.not.exist(act); + done(); + }); + }); + }); + + it('should have deleted related item', function(done) { + Supplier.findById(supplierId, function (e, supplier) { + should.not.exist(e); + should.exist(supplier); + done(); + }); + }); + }); describe('hasAndBelongsToMany', function () { @@ -1320,6 +1435,155 @@ describe('relations', function () { }); }); + describe('embedsOne', function () { + + var person; + var Other; + + before(function () { + db = getSchema(); + Person = db.define('Person', {name: String}); + Passport = db.define('Passport', + {name:{type:'string', required: true}}, + {idInjection: false} + ); + Other = db.define('Other', {name: String}); + }); + + it('can be declared using embedsOne method', function (done) { + Person.embedsOne(Passport, { + default: {name: 'Anonymous'} // a bit contrived + }); + db.automigrate(done); + }); + + it('should have setup a property and accessor', function() { + var p = new Person(); + p.passport.should.be.an.object; // because of default + p.passportItem.should.be.a.function; + p.passportItem.create.should.be.a.function; + p.passportItem.build.should.be.a.function; + p.passportItem.destroy.should.be.a.function; + }); + + it('should return an instance with default values', function() { + var p = new Person(); + p.passport.toObject().should.eql({name: 'Anonymous'}); + p.passportItem().should.equal(p.passport); + p.passportItem(function(err, passport) { + should.not.exist(err); + passport.should.equal(p.passport); + }); + }); + + it('should embed a model instance', function() { + var p = new Person(); + p.passportItem(new Passport({name: 'Fred'})); + p.passport.toObject().should.eql({name: 'Fred'}); + p.passport.should.be.an.instanceOf(Passport); + }); + + it('should not embed an invalid model type', function() { + var p = new Person(); + p.passportItem(new Other()); + p.passport.toObject().should.eql({name: 'Anonymous'}); + p.passport.should.be.an.instanceOf(Passport); + }); + + var personId; + it('should create an embedded item on scope', function(done) { + Person.create({name: 'Fred'}, function(err, p) { + should.not.exist(err); + personId = p.id; + p.passportItem.create({name: 'Fredric'}, function(err, passport) { + should.not.exist(err); + p.passport.toObject().should.eql({name: 'Fredric'}); + p.passport.should.be.an.instanceOf(Passport); + done(); + }); + }); + }); + + it('should get an embedded item on scope', function(done) { + Person.findById(personId, function(err, p) { + should.not.exist(err); + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Fredric'}); + passport.should.be.an.instanceOf(Passport); + passport.should.equal(p.passport); + done(); + }); + }); + + it('should validate an embedded item on scope - on creation', function(done) { + var p = new Person({name: 'Fred'}); + p.passportItem.create({}, function(err, passport) { + should.exist(err); + err.name.should.equal('ValidationError'); + var msg = 'The `Passport` instance is not valid.'; + msg += ' Details: `name` can\'t be blank.'; + err.message.should.equal(msg); + done(); + }); + }); + + it('should validate an embedded item on scope - on update', function(done) { + Person.findById(personId, function(err, p) { + var passport = p.passportItem(); + passport.name = null; + p.save(function(err) { + should.exist(err); + err.name.should.equal('ValidationError'); + var msg = 'The `Person` instance is not valid.'; + msg += ' Details: `passportItem` is invalid: `name` can\'t be blank.'; + err.message.should.equal(msg); + done(); + }); + }); + }); + + it('should update an embedded item on scope', function(done) { + Person.findById(personId, function(err, p) { + p.passportItem.update({name: 'Freddy'}, function(err, passport) { + should.not.exist(err); + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Freddy'}); + passport.should.be.an.instanceOf(Passport); + passport.should.equal(p.passport); + done(); + }); + }); + }); + + it('should get an embedded item on scope - verify', function(done) { + Person.findById(personId, function(err, p) { + should.not.exist(err); + var passport = p.passportItem(); + passport.toObject().should.eql({name: 'Freddy'}); + done(); + }); + }); + + it('should destroy an embedded item on scope', function(done) { + Person.findById(personId, function(err, p) { + p.passportItem.destroy(function(err) { + should.not.exist(err); + should.equal(p.passport, null); + done(); + }); + }); + }); + + it('should get an embedded item on scope - verify', function(done) { + Person.findById(personId, function(err, p) { + should.not.exist(err); + should.equal(p.passport, null); + done(); + }); + }); + + }); + describe('embedsMany', function () { var address1, address2; @@ -1515,7 +1779,7 @@ describe('relations', function () { }); it('can be declared', function (done) { - Person.embedsMany(Address, { options: { autoId: false, validate: true } }); + Person.embedsMany(Address, { options: { autoId: false } }); db.automigrate(done); }); @@ -1591,7 +1855,7 @@ describe('relations', function () { Person.create({ name: 'Wilma', addresses: addresses }, function(err, p) { err.name.should.equal('ValidationError'); var expected = 'The `Person` instance is not valid. '; - expected += 'Details: `addresses` contains invalid item: `work` (street can\'t be blank).'; + expected += 'Details: `addresses` contains invalid item: `work` (`street` can\'t be blank).'; err.message.should.equal(expected); done(); }); @@ -1644,7 +1908,7 @@ describe('relations', function () { db = getSchema(); Category = db.define('Category', {name: String}); Product = db.define('Product', {name: String}); - Link = db.define('Link', {name: String}); + Link = db.define('Link', {name: String, notes: String}); }); it('can be declared', function (done) { @@ -1878,7 +2142,7 @@ describe('relations', function () { Author = db.define('Author', {name: String}); Reader = db.define('Reader', {name: String}); - Link = db.define('Link'); // generic model + Link = db.define('Link', {name: String, notes: String}); // generic model Link.validatesPresenceOf('linkedId'); Link.validatesPresenceOf('linkedType');