Implemented embedsOne
This commit is contained in:
parent
2b262c04f6
commit
7c901af6d9
|
@ -20,6 +20,7 @@ var RelationTypes = {
|
||||||
hasOne: 'hasOne',
|
hasOne: 'hasOne',
|
||||||
hasAndBelongsToMany: 'hasAndBelongsToMany',
|
hasAndBelongsToMany: 'hasAndBelongsToMany',
|
||||||
referencesMany: 'referencesMany',
|
referencesMany: 'referencesMany',
|
||||||
|
embedsOne: 'embedsOne',
|
||||||
embedsMany: 'embedsMany'
|
embedsMany: 'embedsMany'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ exports.HasOne = HasOne;
|
||||||
exports.HasAndBelongsToMany = HasAndBelongsToMany;
|
exports.HasAndBelongsToMany = HasAndBelongsToMany;
|
||||||
exports.BelongsTo = BelongsTo;
|
exports.BelongsTo = BelongsTo;
|
||||||
exports.ReferencesMany = ReferencesMany;
|
exports.ReferencesMany = ReferencesMany;
|
||||||
|
exports.EmbedsOne = EmbedsOne;
|
||||||
exports.EmbedsMany = EmbedsMany;
|
exports.EmbedsMany = EmbedsMany;
|
||||||
|
|
||||||
var RelationClasses = {
|
var RelationClasses = {
|
||||||
|
@ -39,6 +41,7 @@ var RelationClasses = {
|
||||||
hasOne: HasOne,
|
hasOne: HasOne,
|
||||||
hasAndBelongsToMany: HasAndBelongsToMany,
|
hasAndBelongsToMany: HasAndBelongsToMany,
|
||||||
referencesMany: ReferencesMany,
|
referencesMany: ReferencesMany,
|
||||||
|
embedsOne: EmbedsOne,
|
||||||
embedsMany: EmbedsMany
|
embedsMany: EmbedsMany
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -384,6 +387,24 @@ function HasOne(definition, modelInstance) {
|
||||||
|
|
||||||
util.inherits(HasOne, Relation);
|
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
|
* EmbedsMany subclass
|
||||||
* @param {RelationDefinition|Object} definition
|
* @param {RelationDefinition|Object} definition
|
||||||
|
@ -1539,6 +1560,170 @@ 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) {
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
targetModelData = targetModelData || {};
|
||||||
|
|
||||||
|
this.definition.applyProperties(modelInstance, targetModelData);
|
||||||
|
|
||||||
|
return new modelTo(targetModelData);
|
||||||
|
};
|
||||||
|
|
||||||
|
EmbedsOne.prototype.destroy = function (cb) {
|
||||||
|
var modelInstance = this.modelInstance;
|
||||||
|
var propertyName = this.definition.keyFrom;
|
||||||
|
modelInstance.unsetAttribute(propertyName, true);
|
||||||
|
if (typeof cb === 'function') {
|
||||||
|
modelInstance.save(function(err) {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) {
|
RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
|
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
|
||||||
|
@ -1602,7 +1787,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
|
||||||
var id = item[idName] || '(blank)';
|
var id = item[idName] || '(blank)';
|
||||||
var first = Object.keys(item.errors)[0];
|
var first = Object.keys(item.errors)[0];
|
||||||
var msg = 'contains invalid item: `' + id + '`';
|
var msg = 'contains invalid item: `' + id + '`';
|
||||||
msg += ' (' + first + ' ' + item.errors[first] + ')';
|
msg += ' (`' + first + '` ' + item.errors[first] + ')';
|
||||||
self.errors.add(propertyName, msg, 'invalid');
|
self.errors.add(propertyName, msg, 'invalid');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1716,15 +1901,11 @@ EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
|
|
||||||
var actualCond = {};
|
var actualCond = {};
|
||||||
var actualRefresh = false;
|
|
||||||
if (arguments.length === 3) {
|
if (arguments.length === 3) {
|
||||||
cb = condOrRefresh;
|
cb = condOrRefresh;
|
||||||
} else if (arguments.length === 4) {
|
} else if (arguments.length === 4) {
|
||||||
if (typeof condOrRefresh === 'boolean') {
|
if (typeof condOrRefresh === 'object') {
|
||||||
actualRefresh = condOrRefresh;
|
|
||||||
} else {
|
|
||||||
actualCond = condOrRefresh;
|
actualCond = condOrRefresh;
|
||||||
actualRefresh = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Method can be only called with one or two arguments');
|
throw new Error('Method can be only called with one or two arguments');
|
||||||
|
@ -1923,7 +2104,7 @@ EmbedsMany.prototype.build = function(targetModelData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.definition.applyProperties(this.modelInstance, targetModelData);
|
this.definition.applyProperties(modelInstance, targetModelData);
|
||||||
|
|
||||||
var inst = new modelTo(targetModelData);
|
var inst = new modelTo(targetModelData);
|
||||||
|
|
||||||
|
|
|
@ -158,14 +158,18 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params
|
||||||
return RelationDefinition.hasAndBelongsToMany(this, 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);
|
return RelationDefinition.hasOne(this, modelTo, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
RelationMixin.referencesMany = function hasMany(modelTo, params) {
|
RelationMixin.referencesMany = function referencesMany(modelTo, params) {
|
||||||
return RelationDefinition.referencesMany(this, 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);
|
return RelationDefinition.embedsMany(this, modelTo, params);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1320,6 +1320,131 @@ 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 () {
|
||||||
|
Person.embedsOne(Passport, {
|
||||||
|
default: {name: 'Anonymous'}, // a bit contrived
|
||||||
|
options: {validate: true}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an embedded item on scope', function(done) {
|
||||||
|
Person.create({name: 'Fred'}, function(err, p) {
|
||||||
|
should.not.exist(err);
|
||||||
|
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.findOne(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.findOne(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 destroy an embedded item on scope', function(done) {
|
||||||
|
Person.findOne(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.findOne(function(err, p) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.equal(p.passport, null);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('embedsMany', function () {
|
describe('embedsMany', function () {
|
||||||
|
|
||||||
var address1, address2;
|
var address1, address2;
|
||||||
|
@ -1591,7 +1716,7 @@ describe('relations', function () {
|
||||||
Person.create({ name: 'Wilma', addresses: addresses }, function(err, p) {
|
Person.create({ name: 'Wilma', addresses: addresses }, function(err, p) {
|
||||||
err.name.should.equal('ValidationError');
|
err.name.should.equal('ValidationError');
|
||||||
var expected = 'The `Person` instance is not valid. ';
|
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);
|
err.message.should.equal(expected);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue