不要怂,就是干,撸起袖子干!

Commit e9bb2956 by Mick Hansen

feat(association/scope): support for N:M scopes on target model (only for mysql/pg)

1 parent 2f2af764
......@@ -21,6 +21,13 @@ module.exports = (function() {
var instancePrimaryKey = self.instance.Model.primaryKeyAttribute
, foreignPrimaryKey = self.association.target.primaryKeyAttribute;
var scopeWhere = this.association.scope ? {} : null;
if (this.association.scope) {
Object.keys(this.association.scope).forEach(function (attribute) {
scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this));
}
options.where = new Utils.and([
new Utils.where(
through.rawAttributes[self.association.identifier],
......@@ -35,6 +42,7 @@ module.exports = (function() {
].join('.'))
}
),
scopeWhere,
options.where
]);
......
......@@ -458,6 +458,12 @@ module.exports = (function() {
values = {};
}
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
});
}
if (Object(association.through) === association.through) {
// Create the related model instance
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
......@@ -465,11 +471,6 @@ module.exports = (function() {
});
} else {
values[association.identifier] = instance.get(association.source.primaryKeyAttribute);
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
});
}
return association.target.create(values, fieldsOrOptions);
}
};
......
......@@ -232,7 +232,7 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* @param {object} [options.through.scope] A key/value set that will be used for association create and find defaults on the through model. (Remember to add the attributes to the through model)
* @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the assocition, you should provide the same alias when eager loading and when getting assocated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the target table / join table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the colum. Defaults to the name of source + primary key of source
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target.
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {string} [options.onDelete='SET NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
......
......@@ -19,6 +19,7 @@ AbstractDialect.prototype.supports = {
type: false,
using: true,
},
joinTableDependent: true
};
module.exports = AbstractDialect;
......@@ -874,13 +874,23 @@ module.exports = (function() {
targetJoinOn = self.quoteIdentifier(tableTarget) + '.' + self.quoteIdentifier(attrTarget) + ' = ';
targetJoinOn += self.quoteIdentifier(throughAs) + '.' + self.quoteIdentifier(identTarget);
// Generate join SQL for left side of through
joinQueryItem += joinType + self.quoteTable(throughTable, throughAs) + ' ON ';
joinQueryItem += sourceJoinOn;
// Generate join SQL for right side of through
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
if (self._dialect.supports.joinTableDependent) {
// Generate a wrapped join so that the through table join can be dependent on the target join
joinQueryItem += joinType + '(';
joinQueryItem += self.quoteTable(throughTable, throughAs);
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
joinQueryItem += ') ON '+sourceJoinOn;
} else {
// Generate join SQL for left side of through
joinQueryItem += joinType + self.quoteTable(throughTable, throughAs) + ' ON ';
joinQueryItem += sourceJoinOn;
// Generate join SQL for right side of through
joinQueryItem += joinType + self.quoteTable(table, as) + ' ON ';
joinQueryItem += targetJoinOn;
}
if (include.where) {
targetWhere = self.getWhereConditions(include.where, self.sequelize.literal(self.quoteIdentifier(as)), include.model, whereOptions);
......
......@@ -15,7 +15,8 @@ SqliteDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.suppor
'DEFAULT VALUES': true,
index: {
using: false
}
},
joinTableDependent: false
});
SqliteDialect.prototype.Query = Query;
......
......@@ -159,13 +159,129 @@ describe(Support.getTestDialectTeaser("associations"), function() {
});
});
});
if (Support.getTestDialect() !== 'sqlite') {
describe('N:M', function () {
describe('on the target', function () {
beforeEach(function () {
this.Post = this.sequelize.define('post', {});
this.Tag = this.sequelize.define('tag', {
type: DataTypes.STRING
});
this.PostTag = this.sequelize.define('post_tag');
describe('N:M', function () {
describe('on the target', function () {
it('should create, find and include associations with scope values', function () {
this.Tag.hasMany(this.Post, {through: this.PostTag});
this.Post.hasMany(this.Tag, {as: 'categories', through: this.PostTag, scope: { type: 'category' }});
this.Post.hasMany(this.Tag, {as: 'tags', through: this.PostTag, scope: { type: 'tag' }});
});
it('should create, find and include associations with scope values', function () {
var self = this;
return Promise.join(
self.Post.sync({force: true}),
self.Tag.sync({force: true})
).bind(this).then(function () {
return self.PostTag.sync({force: true});
}).then(function () {
return Promise.join(
self.Post.create(),
self.Post.create(),
self.Post.create(),
self.Tag.create({type: 'category'}),
self.Tag.create({type: 'category'}),
self.Tag.create({type: 'tag'}),
self.Tag.create({type: 'tag'})
);
}).spread(function (postA, postB, postC, categoryA, categoryB, tagA, tagB) {
this.postA = postA;
this.postB = postB;
this.postC = postC;
return Promise.join(
postA.addCategory(categoryA),
postB.setCategories([categoryB]),
postC.createCategory(),
postA.createTag(),
postB.addTag(tagA),
postC.setTags([tagB])
);
}).then(function () {
return Promise.join(
this.postA.getCategories(),
this.postA.getTags(),
this.postB.getCategories(),
this.postB.getTags(),
this.postC.getCategories(),
this.postC.getTags()
);
}).spread(function (postACategories, postATags, postBCategories, postBTags, postCCategories, postCTags) {
expect(postACategories.length).to.equal(1);
expect(postATags.length).to.equal(1);
expect(postBCategories.length).to.equal(1);
expect(postBTags.length).to.equal(1);
expect(postCCategories.length).to.equal(1);
expect(postCTags.length).to.equal(1);
expect(postACategories[0].get('type')).to.equal('category');
expect(postATags[0].get('type')).to.equal('tag');
expect(postBCategories[0].get('type')).to.equal('category');
expect(postBTags[0].get('type')).to.equal('tag');
expect(postCCategories[0].get('type')).to.equal('category');
expect(postCTags[0].get('type')).to.equal('tag');
}).then(function () {
return Promise.join(
self.Post.find({
where: {
id: self.postA.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
}),
self.Post.find({
where: {
id: self.postB.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
}),
self.Post.find({
where: {
id: self.postC.get('id')
},
include: [
{model: self.Tag, as: 'tags'},
{model: self.Tag, as: 'categories'}
]
})
);
}).spread(function (postA, postB, postC) {
//console.log(postA.get('categories'));
expect(postA.get('categories').length).to.equal(1);
expect(postA.get('tags').length).to.equal(1);
expect(postB.get('categories').length).to.equal(1);
expect(postB.get('tags').length).to.equal(1);
expect(postC.get('categories').length).to.equal(1);
expect(postC.get('tags').length).to.equal(1);
expect(postA.get('categories')[0].get('type')).to.equal('category');
expect(postA.get('tags')[0].get('type')).to.equal('tag');
expect(postB.get('categories')[0].get('type')).to.equal('category');
expect(postB.get('tags')[0].get('type')).to.equal('tag');
expect(postC.get('categories')[0].get('type')).to.equal('category');
expect(postC.get('tags')[0].get('type')).to.equal('tag');
});
});
});
describe.skip('on the through model', function () {
it('should create, find and include associations with scope values', function () {
});
});
});
});
}
});
});
\ No newline at end of file
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!