Implemented embedsOne
This commit is contained in:
parent
2b262c04f6
commit
7c901af6d9
|
@ -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
|
||||
|
@ -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) {
|
||||
params = params || {};
|
||||
modelTo = lookupModelTo(modelFrom, modelTo, params, true);
|
||||
|
@ -1602,7 +1787,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 +1901,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 +2104,7 @@ EmbedsMany.prototype.build = function(targetModelData) {
|
|||
}
|
||||
}
|
||||
|
||||
this.definition.applyProperties(this.modelInstance, targetModelData);
|
||||
this.definition.applyProperties(modelInstance, targetModelData);
|
||||
|
||||
var inst = new modelTo(targetModelData);
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 () {
|
||||
|
||||
var address1, address2;
|
||||
|
@ -1591,7 +1716,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();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue