Merge branch 'release/2.5.0' into production

This commit is contained in:
Raymond Feng 2014-08-20 15:09:28 -07:00
commit 7b00635d82
8 changed files with 608 additions and 42 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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 ) {

View File

@ -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/<model>/<id>/<embedsOneRelationName>
// 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);

View File

@ -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);
};

View File

@ -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);
};

View File

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

View File

@ -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');