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;
|
var constr = this;
|
||||||
this.schema.adapter.all(this.modelName, params, function (err, data) {
|
this.schema.adapter.all(this.modelName, params, function (err, data) {
|
||||||
if (data && data.map) {
|
if (data && data.forEach) {
|
||||||
data.forEach(function (d, i) {
|
data.forEach(function (d, i) {
|
||||||
var obj = new constr;
|
var obj = new constr;
|
||||||
obj._initProperties(d, false);
|
obj._initProperties(d, false);
|
||||||
|
if (params && params.include && params.collect) {
|
||||||
|
data[i] = obj.__cachedRelations[params.collect];
|
||||||
|
} else {
|
||||||
data[i] = obj;
|
data[i] = obj;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (data && data.countBeforeLimit) {
|
if (data && data.countBeforeLimit) {
|
||||||
data.countBeforeLimit = data.countBeforeLimit;
|
data.countBeforeLimit = data.countBeforeLimit;
|
||||||
|
|
116
lib/relations.js
116
lib/relations.js
|
@ -7,16 +7,16 @@ var defineScope = require('./scope.js').defineScope;
|
||||||
/**
|
/**
|
||||||
* Relations mixins for ./model.js
|
* Relations mixins for ./model.js
|
||||||
*/
|
*/
|
||||||
var AbstractClass = require('./model.js');
|
var Model = require('./model.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Declare hasMany relation
|
* Declare hasMany relation
|
||||||
*
|
*
|
||||||
* @param {Class} anotherClass - class to has many
|
* @param {Model} anotherClass - class to has many
|
||||||
* @param {Object} params - configuration {as:, foreignKey:}
|
* @param {Object} params - configuration {as:, foreignKey:}
|
||||||
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
|
* @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;
|
var thisClass = this, thisClassName = this.modelName;
|
||||||
params = params || {};
|
params = params || {};
|
||||||
if (typeof anotherClass === 'string') {
|
if (typeof anotherClass === 'string') {
|
||||||
|
@ -46,14 +46,65 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
|
||||||
// each instance of this class should have method named
|
// each instance of this class should have method named
|
||||||
// pluralize(anotherClass.modelName)
|
// pluralize(anotherClass.modelName)
|
||||||
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
|
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
|
||||||
defineScope(this.prototype, anotherClass, methodName, function () {
|
var scopeMethods = {
|
||||||
var x = {};
|
|
||||||
x[fk] = this.id;
|
|
||||||
return {where: x};
|
|
||||||
}, {
|
|
||||||
find: find,
|
find: find,
|
||||||
destroy: destroy
|
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`
|
// obviously, anotherClass should have attribute called `fk`
|
||||||
anotherClass.schema.defineForeignKey(anotherClass.modelName, 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.
|
* 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 || {};
|
params = params || {};
|
||||||
if ('string' === typeof anotherClass) {
|
if ('string' === typeof anotherClass) {
|
||||||
params.as = anotherClass;
|
params.as = anotherClass;
|
||||||
|
@ -124,7 +175,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
||||||
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
|
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
|
||||||
var fk = params.foreignKey || methodName + 'Id';
|
var fk = params.foreignKey || methodName + 'Id';
|
||||||
|
|
||||||
this.relations[params['as']] = {
|
this.relations[methodName] = {
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
keyFrom: fk,
|
keyFrom: fk,
|
||||||
keyTo: 'id',
|
keyTo: 'id',
|
||||||
|
@ -163,7 +214,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
|
||||||
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
|
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
|
||||||
cachedValue = this.__cachedRelations[methodName];
|
cachedValue = this.__cachedRelations[methodName];
|
||||||
}
|
}
|
||||||
if (p instanceof AbstractClass) { // acts as setter
|
if (p instanceof Model) { // acts as setter
|
||||||
this[fk] = p.id;
|
this[fk] = p.id;
|
||||||
this.__cachedRelations[methodName] = p;
|
this.__cachedRelations[methodName] = p;
|
||||||
} else if (typeof p === 'function') { // acts as async getter
|
} 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) {
|
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
|
||||||
var self = this;
|
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 (!err && saveOnCache) {
|
||||||
if (!self.__cachedRelations) {
|
if (!self.__cachedRelations) {
|
||||||
self.__cachedRelations = {};
|
self.__cachedRelations = {};
|
||||||
|
@ -134,6 +135,12 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
if (update.where) {
|
if (update.where) {
|
||||||
base.where = merge(base.where, 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
|
// overwrite order
|
||||||
if (update.order) {
|
if (update.order) {
|
||||||
|
|
|
@ -159,13 +159,78 @@ describe('relations', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip('hasAndBelongsToMany', function() {
|
describe('hasAndBelongsToMany', function() {
|
||||||
var Article, Tag;
|
var Article, Tag, ArticleTag;
|
||||||
it('can be declared', function(done) {
|
it('can be declared', function(done) {
|
||||||
Article = db.define('Article', {title: String});
|
Article = db.define('Article', {title: String});
|
||||||
Tag = db.define('Tag', {name: String});
|
Tag = db.define('Tag', {name: String});
|
||||||
Article.hasAndBelongsToMany('tags');
|
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;
|
var db, Railway, Station;
|
||||||
|
|
||||||
describe('sc0pe', function() {
|
describe('sc0pe', function() {
|
||||||
|
|
||||||
before(function() {
|
before(function() {
|
||||||
db = getSchema();
|
db = getSchema();
|
||||||
Railway = db.define('Railway', {
|
Railway = db.define('Railway', {
|
||||||
|
@ -16,7 +17,6 @@ describe('sc0pe', function() {
|
||||||
isActive: {type: Boolean, index: true},
|
isActive: {type: Boolean, index: true},
|
||||||
isUndeground: {type: Boolean, index: true}
|
isUndeground: {type: Boolean, index: true}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
|
@ -27,7 +27,6 @@ describe('sc0pe', function() {
|
||||||
|
|
||||||
it('should define scope with query', function(done) {
|
it('should define scope with query', function(done) {
|
||||||
Station.scope('active', {where: {isActive: true}});
|
Station.scope('active', {where: {isActive: true}});
|
||||||
|
|
||||||
Station.active.create(function(err, station) {
|
Station.active.create(function(err, station) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
should.exist(station);
|
should.exist(station);
|
||||||
|
|
Loading…
Reference in New Issue