Many-to-many relation
- hasMany {through: Class} - hasAndBelongsToMany - some specs in relations.test.js
This commit is contained in:
parent
9facf369b1
commit
c9e97744dd
|
@ -388,11 +388,15 @@ AbstractClass.all = function all(params, cb) {
|
|||
}
|
||||
var constr = this;
|
||||
this.schema.adapter.all(this.modelName, params, function (err, data) {
|
||||
if (data && data.map) {
|
||||
if (data && data.forEach) {
|
||||
data.forEach(function (d, i) {
|
||||
var obj = new constr;
|
||||
obj._initProperties(d, false);
|
||||
data[i] = obj;
|
||||
if (params && params.include && params.collect) {
|
||||
data[i] = obj.__cachedRelations[params.collect];
|
||||
} else {
|
||||
data[i] = obj;
|
||||
}
|
||||
});
|
||||
if (data && data.countBeforeLimit) {
|
||||
data.countBeforeLimit = data.countBeforeLimit;
|
||||
|
|
118
lib/relations.js
118
lib/relations.js
|
@ -7,16 +7,16 @@ var defineScope = require('./scope.js').defineScope;
|
|||
/**
|
||||
* Relations mixins for ./model.js
|
||||
*/
|
||||
var AbstractClass = require('./model.js');
|
||||
var Model = require('./model.js');
|
||||
|
||||
/**
|
||||
* Declare hasMany relation
|
||||
*
|
||||
* @param {Class} anotherClass - class to has many
|
||||
* @param {Model} anotherClass - class to has many
|
||||
* @param {Object} params - configuration {as:, foreignKey:}
|
||||
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
|
||||
*/
|
||||
AbstractClass.hasMany = function hasMany(anotherClass, params) {
|
||||
Model.hasMany = function hasMany(anotherClass, params) {
|
||||
var thisClass = this, thisClassName = this.modelName;
|
||||
params = params || {};
|
||||
if (typeof anotherClass === 'string') {
|
||||
|
@ -46,14 +46,65 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
|
|||
// each instance of this class should have method named
|
||||
// pluralize(anotherClass.modelName)
|
||||
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
|
||||
defineScope(this.prototype, anotherClass, methodName, function () {
|
||||
var x = {};
|
||||
x[fk] = this.id;
|
||||
return {where: x};
|
||||
}, {
|
||||
var scopeMethods = {
|
||||
find: find,
|
||||
destroy: destroy
|
||||
});
|
||||
};
|
||||
if (params.through) {
|
||||
var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
|
||||
scopeMethods.create = function create(data, done) {
|
||||
if (typeof data !== 'object') {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
var id = this.id;
|
||||
anotherClass.create(data, function(err, ac) {
|
||||
if (err) return done(err, ac);
|
||||
var d = {};
|
||||
d[fk] = id;
|
||||
d[fk2] = ac.id;
|
||||
params.through.create(d, function(e) {
|
||||
if (e) {
|
||||
ac.destroy(function() {
|
||||
done(e);
|
||||
});
|
||||
} else {
|
||||
done(err, ac);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
scopeMethods.add = function(acInst, done) {
|
||||
var data = {};
|
||||
data[fk] = this.id;
|
||||
data[fk2] = acInst.id || acInst;
|
||||
params.through.findOrCreate({where: data}, done);
|
||||
};
|
||||
scopeMethods.remove = function(acInst, done) {
|
||||
var self = this;
|
||||
var q = {};
|
||||
q[fk2] = acInst.id || acInst;
|
||||
params.through.findOne({where: q}, function(err, d) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (!d) {
|
||||
return done();
|
||||
}
|
||||
d.destroy(done);
|
||||
});
|
||||
};
|
||||
}
|
||||
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
||||
var filter = {};
|
||||
filter.where = {};
|
||||
filter.where[fk] = this.id;
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(anotherClass.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
}
|
||||
return filter;
|
||||
}, scopeMethods);
|
||||
|
||||
// obviously, anotherClass should have attribute called `fk`
|
||||
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
|
||||
|
@ -106,7 +157,7 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
|
|||
*
|
||||
* This optional parameter default value is false, so the related object will be loaded from cache if available.
|
||||
*/
|
||||
AbstractClass.belongsTo = function (anotherClass, params) {
|
||||
Model.belongsTo = function (anotherClass, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
|
@ -124,7 +175,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
|||
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
|
||||
var fk = params.foreignKey || methodName + 'Id';
|
||||
|
||||
this.relations[params['as']] = {
|
||||
this.relations[methodName] = {
|
||||
type: 'belongsTo',
|
||||
keyFrom: fk,
|
||||
keyTo: 'id',
|
||||
|
@ -163,7 +214,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
|||
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
|
||||
cachedValue = this.__cachedRelations[methodName];
|
||||
}
|
||||
if (p instanceof AbstractClass) { // acts as setter
|
||||
if (p instanceof Model) { // acts as setter
|
||||
this[fk] = p.id;
|
||||
this.__cachedRelations[methodName] = p;
|
||||
} else if (typeof p === 'function') { // acts as async getter
|
||||
|
@ -189,3 +240,46 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
|||
|
||||
};
|
||||
|
||||
/**
|
||||
* Many-to-many relation
|
||||
*
|
||||
* Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag'
|
||||
*/
|
||||
Model.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
||||
params = params || {};
|
||||
var models = this.schema.models;
|
||||
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
anotherClass = lookupModel(i8n.singularize(anotherClass)) ||
|
||||
anotherClass;
|
||||
}
|
||||
if (typeof anotherClass === 'string') {
|
||||
throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.through) {
|
||||
var name1 = this.modelName + anotherClass.modelName;
|
||||
var name2 = anotherClass.modelName + this.modelName;
|
||||
params.through = lookupModel(name1) || lookupModel(name2) ||
|
||||
this.schema.define(name1);
|
||||
}
|
||||
params.through.belongsTo(this);
|
||||
params.through.belongsTo(anotherClass);
|
||||
|
||||
this.hasMany(anotherClass, {as: params.as, through: params.through});
|
||||
|
||||
function lookupModel(modelName) {
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -57,7 +57,8 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
|
||||
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
|
||||
var self = this;
|
||||
return targetClass.all(mergeParams(actualCond, caller._scope), function(err, data) {
|
||||
var params = mergeParams(actualCond, caller._scope);
|
||||
return targetClass.all(params, function(err, data) {
|
||||
if (!err && saveOnCache) {
|
||||
if (!self.__cachedRelations) {
|
||||
self.__cachedRelations = {};
|
||||
|
@ -134,6 +135,12 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
if (update.where) {
|
||||
base.where = merge(base.where, update.where);
|
||||
}
|
||||
if (update.include) {
|
||||
base.include = update.include;
|
||||
}
|
||||
if (update.collect) {
|
||||
base.collect = update.collect;
|
||||
}
|
||||
|
||||
// overwrite order
|
||||
if (update.order) {
|
||||
|
|
|
@ -159,13 +159,78 @@ describe('relations', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe.skip('hasAndBelongsToMany', function() {
|
||||
var Article, Tag;
|
||||
describe('hasAndBelongsToMany', function() {
|
||||
var Article, Tag, ArticleTag;
|
||||
it('can be declared', function(done) {
|
||||
Article = db.define('Article', {title: String});
|
||||
Tag = db.define('Tag', {name: String});
|
||||
Article.hasAndBelongsToMany('tags');
|
||||
ArticleTag = db.models.ArticleTag;
|
||||
db.automigrate(function() {
|
||||
Article.destroyAll(function() {
|
||||
Tag.destroyAll(function() {
|
||||
ArticleTag.destroyAll(done)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to create instances on scope', function(done) {
|
||||
Article.create(function(e, article) {
|
||||
article.tags.create({name: 'popular'}, function(e, t) {
|
||||
t.should.be.an.instanceOf(Tag);
|
||||
ArticleTag.findOne(function(e, at) {
|
||||
should.exist(at);
|
||||
at.tagId.should.equal(t.id);
|
||||
at.articleId.should.equal(article.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to fetch scoped instances', function(done) {
|
||||
Article.findOne(function(e, article) {
|
||||
article.tags(function(e, tags) {
|
||||
should.not.exist(e);
|
||||
should.exist(tags);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to add connection with instance', function(done) {
|
||||
Article.findOne(function(e, article) {
|
||||
Tag.create({name: 'awesome'}, function(e, tag) {
|
||||
article.tags.add(tag, function(e, at) {
|
||||
should.not.exist(e);
|
||||
should.exist(at);
|
||||
at.should.be.an.instanceOf(ArticleTag);
|
||||
at.tagId.should.equal(tag.id);
|
||||
at.articleId.should.equal(article.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to remove connection with instance', function(done) {
|
||||
Article.findOne(function(e, article) {
|
||||
article.tags(function(e, tags) {
|
||||
var len = tags.length;
|
||||
article.tags.remove(tags[0], function(e, at) {
|
||||
should.not.exist(e);
|
||||
article.tags(true, function(e, tags) {
|
||||
tags.should.have.lengthOf(len - 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to destroy instance and connection');
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ var should = require('./init.js');
|
|||
var db, Railway, Station;
|
||||
|
||||
describe('sc0pe', function() {
|
||||
|
||||
before(function() {
|
||||
db = getSchema();
|
||||
Railway = db.define('Railway', {
|
||||
|
@ -16,7 +17,6 @@ describe('sc0pe', function() {
|
|||
isActive: {type: Boolean, index: true},
|
||||
isUndeground: {type: Boolean, index: true}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
beforeEach(function(done) {
|
||||
|
@ -27,7 +27,6 @@ describe('sc0pe', function() {
|
|||
|
||||
it('should define scope with query', function(done) {
|
||||
Station.scope('active', {where: {isActive: true}});
|
||||
|
||||
Station.active.create(function(err, station) {
|
||||
should.not.exist(err);
|
||||
should.exist(station);
|
||||
|
|
Loading…
Reference in New Issue