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

Commit f191bd3b by Sushant Committed by GitHub

Feat 4597 : Pass through values using options.through for N:M relationships (#6931)

* feat: set/add/create through model with options.through

* docs: options.through for set/add/create N:M

* [ci skip] changelog
1 parent b6e1c154
# Future # Future
- [FIXED] N:M `through` option naming collisions [#4597](https://github.com/sequelize/sequelize/issues/4597)
[#6444](https://github.com/sequelize/sequelize/issues/6444)
- [CHANGED] Updated deprecated `node-uuid` package to `uuid` [#6919](https://github.com/sequelize/sequelize/pull/6919) - [CHANGED] Updated deprecated `node-uuid` package to `uuid` [#6919](https://github.com/sequelize/sequelize/pull/6919)
- [ADDED] UPSERT Support for MSSQL [#6842](https://github.com/sequelize/sequelize/pull/6842) - [ADDED] UPSERT Support for MSSQL [#6842](https://github.com/sequelize/sequelize/pull/6842)
- [FIXED] Execute queries parallel in findAndCount [#6695](https://github.com/sequelize/sequelize/issues/6695) - [FIXED] Execute queries parallel in findAndCount [#6695](https://github.com/sequelize/sequelize/issues/6695)
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
## BC breaks: ## BC breaks:
- `DATEONLY` now returns string in `YYYY-MM-DD` format rather than `Date` type - `DATEONLY` now returns string in `YYYY-MM-DD` format rather than `Date` type
- With `BelongsToMany` relationships `add/set/create` setters now set `through` attributes by passing them as `options.through` (previously second argument was used as `through` attributes, now its considered `options` with `through` being a sub option)
# 4.0.0-2 # 4.0.0-2
- [ADDED] include now supports string as an argument (on top of model/association), string will expand into an association matched literally from Model.associations - [ADDED] include now supports string as an argument (on top of model/association), string will expand into an association matched literally from Model.associations
......
...@@ -195,4 +195,4 @@ Count everything currently associated with this, using an optional where clause. ...@@ -195,4 +195,4 @@ Count everything currently associated with this, using an optional where clause.
*** ***
_This document is automatically generated based on source code comments. Please do not edit it directly, as your changes will be ignored. Please write on <a href="irc://irc.freenode.net/#sequelizejs">IRC</a>, open an issue or a create a pull request if you feel something can be improved. For help on how to write source code documentation see [JSDoc](http://usejsdoc.org) and [dox](https://github.com/tj/dox)_ _This document is automatically generated based on source code comments. Please do not edit it directly, as your changes will be ignored. Please write on <a href="irc://irc.freenode.net/#sequelizejs">IRC</a>, open an issue or a create a pull request if you feel something can be improved. For help on how to write source code documentation see [JSDoc](http://usejsdoc.org) and [dox](https://github.com/tj/dox)_
\ No newline at end of file
...@@ -240,10 +240,10 @@ User.belongsToMany(Project, { through: UserProjects }) ...@@ -240,10 +240,10 @@ User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects }) Project.belongsToMany(User, { through: UserProjects })
``` ```
To add a new project to a user and set its status, you pass an extra object to the setter, which contains the attributes for the join table To add a new project to a user and set its status, you pass extra `options.through` to the setter, which contains the attributes for the join table
```js ```js
user.addProject(project, { status: 'started' }) user.addProject(project, { through: { status: 'started' }})
``` ```
By default the code above will add projectId and userId to the UserProjects table, and _remove any previously defined primary key attribute_ - the table will be uniquely identified by the combination of the keys of the two tables, and there is no reason to have other PK columns. To enforce a primary key on the `UserProjects` model you can add it manually. By default the code above will add projectId and userId to the UserProjects table, and _remove any previously defined primary key attribute_ - the table will be uniquely identified by the combination of the keys of the two tables, and there is no reason to have other PK columns. To enforce a primary key on the `UserProjects` model you can add it manually.
...@@ -542,8 +542,8 @@ project.UserProjects = { ...@@ -542,8 +542,8 @@ project.UserProjects = {
} }
u.addProject(project) u.addProject(project)
   
// Or by providing a second argument when adding the association, containing the data that should go in the join table // Or by providing a second options.through argument when adding the association, containing the data that should go in the join table
u.addProject(project, { status: 'active' }) u.addProject(project, { through: { status: 'active' }})
   
   
// When associating multiple objects, you can combine the two options above. In this case the second argument // When associating multiple objects, you can combine the two options above. In this case the second argument
...@@ -552,7 +552,7 @@ project1.UserProjects = { ...@@ -552,7 +552,7 @@ project1.UserProjects = {
status: 'inactive' status: 'inactive'
} }
   
u.setProjects([project1, project2], { status: 'active' }) u.setProjects([project1, project2], { through: { status: 'active' }})
// The code above will record inactive for project one, and active for project two in the join table // The code above will record inactive for project one, and active for project two in the join table
``` ```
...@@ -770,9 +770,9 @@ return Product.create({ ...@@ -770,9 +770,9 @@ return Product.create({
}] }]
} }
}, { }, {
include: [{ include: [{
association: Product.User, association: Product.User,
include: [ User.Addresses ] include: [ User.Addresses ]
}] }]
}); });
``` ```
......
...@@ -21,7 +21,7 @@ const HasOne = require('./has-one'); ...@@ -21,7 +21,7 @@ const HasOne = require('./has-one');
* Project.belongsToMany(User, { through: UserProject }); * Project.belongsToMany(User, { through: UserProject });
* // through is required! * // through is required!
* *
* user.addProject(project, { role: 'manager', transaction: t }); * user.addProject(project, { through: { role: 'manager', transaction: t }});
* ``` * ```
* *
* All methods allow you to pass either a persisted instance, its primary key, or a mixture: * All methods allow you to pass either a persisted instance, its primary key, or a mixture:
...@@ -217,8 +217,9 @@ class BelongsToMany extends Association { ...@@ -217,8 +217,9 @@ class BelongsToMany extends Association {
* Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated. * Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated.
* *
* @param {Array<Model|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations. * @param {Array<Model|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`. Can also hold additional attributes for the join table * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`
* @param {Object} [options.validate] Run validation for the join model * @param {Object} [options.validate] Run validation for the join model
* @param {Object} [options.through] Additional attributes for the join table.
* @return {Promise} * @return {Promise}
* @method setAssociations * @method setAssociations
* @memberof Associations.BelongsToMany * @memberof Associations.BelongsToMany
...@@ -229,8 +230,9 @@ class BelongsToMany extends Association { ...@@ -229,8 +230,9 @@ class BelongsToMany extends Association {
* Associate several persisted instances with this. * Associate several persisted instances with this.
* *
* @param {Array<Model|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. * @param {Array<Model|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table. * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`
* @param {Object} [options.validate] Run validation for the join model. * @param {Object} [options.validate] Run validation for the join model.
* @param {Object} [options.through] Additional attributes for the join table.
* @return {Promise} * @return {Promise}
* @method addAssociations * @method addAssociations
* @memberof Associations.BelongsToMany * @memberof Associations.BelongsToMany
...@@ -241,8 +243,9 @@ class BelongsToMany extends Association { ...@@ -241,8 +243,9 @@ class BelongsToMany extends Association {
* Associate a persisted instance with this. * Associate a persisted instance with this.
* *
* @param {Model|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this. * @param {Model|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table. * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`
* @param {Object} [options.validate] Run validation for the join model. * @param {Object} [options.validate] Run validation for the join model.
* @param {Object} [options.through] Additional attributes for the join table.
* @return {Promise} * @return {Promise}
* @method addAssociation * @method addAssociation
* @memberof Associations.BelongsToMany * @memberof Associations.BelongsToMany
...@@ -253,7 +256,8 @@ class BelongsToMany extends Association { ...@@ -253,7 +256,8 @@ class BelongsToMany extends Association {
* Create a new instance of the associated model and associate it with this. * Create a new instance of the associated model and associate it with this.
* *
* @param {Object} [values] * @param {Object} [values]
* @param {Object} [options] Options passed to create and add. Can also hold additional attributes for the join table * @param {Object} [options] Options passed to create and add
* @param {Object} [options.through] Additional attributes for the join table
* @return {Promise} * @return {Promise}
* @method createAssociation * @method createAssociation
* @memberof Associations.BelongsToMany * @memberof Associations.BelongsToMany
...@@ -585,7 +589,7 @@ class BelongsToMany extends Association { ...@@ -585,7 +589,7 @@ class BelongsToMany extends Association {
return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => { return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => {
const obsoleteAssociations = []; const obsoleteAssociations = [];
const promises = []; const promises = [];
let defaultAttributes = options; let defaultAttributes = options.through || {};
// Don't try to insert the transaction as an attribute in the through table // Don't try to insert the transaction as an attribute in the through table
defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']); defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
...@@ -647,19 +651,18 @@ class BelongsToMany extends Association { ...@@ -647,19 +651,18 @@ class BelongsToMany extends Association {
}); });
} }
add(sourceInstance, newInstances, additionalAttributes) { add(sourceInstance, newInstances, options) {
// If newInstances is null or undefined, no-op // If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve(); if (!newInstances) return Utils.Promise.resolve();
additionalAttributes = _.clone(additionalAttributes) || {}; options = _.clone(options) || {};
const association = this; const association = this;
const defaultAttributes = _.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
const sourceKey = association.source.primaryKeyAttribute; const sourceKey = association.source.primaryKeyAttribute;
const targetKey = association.target.primaryKeyAttribute; const targetKey = association.target.primaryKeyAttribute;
const identifier = association.identifier; const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier; const foreignIdentifier = association.foreignIdentifier;
const options = additionalAttributes; const defaultAttributes = _.omit(options.through || {}, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
newInstances = association.toInstanceArray(newInstances); newInstances = association.toInstanceArray(newInstances);
......
'use strict';
/* /*
* Copy this file to ./sscce.js * Copy this file to ./sscce.js
* Add code from issue * Add code from issue
* npm run sscce-{dialect} * npm run sscce-{dialect}
*/ */
var Sequelize = require('./index'); const Sequelize = require('./index');
var sequelize = require('./test/support').createSequelizeInstance(); const sequelize = require('./test/support').createSequelizeInstance();
...@@ -208,7 +208,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -208,7 +208,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.u = u; this.u = u;
return AcmeProject.create(); return AcmeProject.create();
}).then(function(p) { }).then(function(p) {
return this.u.addProject(p, { status: 'active', data: 42 }); return this.u.addProject(p, { through: { status: 'active', data: 42 }});
}).then(function() { }).then(function() {
return this.u.getProjects(); return this.u.getProjects();
}).then(function(projects) { }).then(function(projects) {
...@@ -489,12 +489,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -489,12 +489,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
return Promise.join( return Promise.join(
this.Task.create().then(function (task) { this.Task.create().then(function (task) {
return user.addTask(task, { return user.addTask(task, {
started: true through: { started: true }
}); });
}), }),
this.Task.create().then(function (task) { this.Task.create().then(function (task) {
return user.addTask(task, { return user.addTask(task, {
started: true through: { started: true }
}); });
}) })
).then(function () { ).then(function () {
...@@ -687,8 +687,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -687,8 +687,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
return Group.create({}); return Group.create({});
}).then(function(group) { }).then(function(group) {
return Promise.join( return Promise.join(
group.createUser({ id: 1 }, { isAdmin: true }), group.createUser({ id: 1 }, { through: {isAdmin: true }}),
group.createUser({ id: 2 }, { isAdmin: false }), group.createUser({ id: 2 }, { through: {isAdmin: false }}),
function() { function() {
return UserGroups.findAll(); return UserGroups.findAll();
} }
...@@ -810,9 +810,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -810,9 +810,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.task = task; this.task = task;
this.user = user; this.user = user;
this.t = t; this.t = t;
return task.addUser(user, { status: 'pending' }); // Create without transaction, so the old value is accesible from outside the transaction return task.addUser(user, { through: {status: 'pending'} }); // Create without transaction, so the old value is accesible from outside the transaction
}).then(function() { }).then(function() {
return this.task.addUser(this.user, { transaction: this.t, status: 'completed' }); // Add an already exisiting user in a transaction, updating a value in the join table return this.task.addUser(this.user, { transaction: this.t, through: {status: 'completed'}}); // Add an already exisiting user in a transaction, updating a value in the join table
}).then(function() { }).then(function() {
return Promise.all([ return Promise.all([
this.user.getTasks(), this.user.getTasks(),
...@@ -991,15 +991,15 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -991,15 +991,15 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
}); });
it('runs on add', function () { it('runs on add', function () {
return expect(this.project.addParticipant(this.employee, { role: ''})).to.be.rejected; return expect(this.project.addParticipant(this.employee, { through: {role: ''}})).to.be.rejected;
}); });
it('runs on set', function () { it('runs on set', function () {
return expect(this.project.setParticipants([this.employee], { role: ''})).to.be.rejected; return expect(this.project.setParticipants([this.employee], { through: {role: ''}})).to.be.rejected;
}); });
it('runs on create', function () { it('runs on create', function () {
return expect(this.project.createParticipant({ name: 'employee 2'}, { role: ''})).to.be.rejected; return expect(this.project.createParticipant({ name: 'employee 2'}, { through: {role: ''}})).to.be.rejected;
}); });
}); });
...@@ -1453,7 +1453,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -1453,7 +1453,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.User.create(), this.User.create(),
this.Project.create() this.Project.create()
]).spread(function(user, project) { ]).spread(function(user, project) {
return user.addProject(project, { status: 'active', data: 42 }).return (user); return user.addProject(project, { through: { status: 'active', data: 42 }}).return (user);
}).then(function(user) { }).then(function(user) {
return user.getProjects(); return user.getProjects();
}).then(function(projects) { }).then(function(projects) {
...@@ -1471,7 +1471,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -1471,7 +1471,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.User.create(), this.User.create(),
this.Project.create() this.Project.create()
]).spread(function(user, project) { ]).spread(function(user, project) {
return user.addProject(project, { status: 'active', data: 42 }).return (user); return user.addProject(project, { through: { status: 'active', data: 42 }}).return (user);
}).then(function(user) { }).then(function(user) {
return user.getProjects({ joinTableAttributes: ['status']}); return user.getProjects({ joinTableAttributes: ['status']});
}).then(function(projects) { }).then(function(projects) {
...@@ -1512,7 +1512,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -1512,7 +1512,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.u = u; this.u = u;
this.p = p; this.p = p;
return u.addProject(p, { status: 'active' }); return u.addProject(p, { through: { status: 'active' }});
}).then(function() { }).then(function() {
return this.UserProjects.findOne({ where: { UserId: this.u.id, ProjectId: this.p.id }}); return this.UserProjects.findOne({ where: { UserId: this.u.id, ProjectId: this.p.id }});
}).then(function(up) { }).then(function(up) {
...@@ -1599,7 +1599,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -1599,7 +1599,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
this.p1.UserProjects = { status: 'inactive' }; this.p1.UserProjects = { status: 'inactive' };
return user.setProjects([this.p1, this.p2], { status: 'active' }); return user.setProjects([this.p1, this.p2], { through: { status: 'active' }});
}).then(function() { }).then(function() {
return Promise.all([ return Promise.all([
self.UserProjects.findOne({ where: { UserId: this.user.id, ProjectId: this.p1.id }}), self.UserProjects.findOne({ where: { UserId: this.user.id, ProjectId: this.p1.id }}),
...@@ -1638,9 +1638,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -1638,9 +1638,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
it('should support query the through model', function () { it('should support query the through model', function () {
return this.User.create().then(function (user) { return this.User.create().then(function (user) {
return Promise.all([ return Promise.all([
user.createProject({}, { status: 'active', data: 1 }), user.createProject({}, { through: { status: 'active', data: 1 }}),
user.createProject({}, { status: 'inactive', data: 2 }), user.createProject({}, { through: { status: 'inactive', data: 2 }}),
user.createProject({}, { status: 'inactive', data: 3 }) user.createProject({}, { through: { status: 'inactive', data: 3 }})
]).then(function () { ]).then(function () {
return Promise.all([ return Promise.all([
user.getProjects({ through: { where: { status: 'active' } } }), user.getProjects({ through: { where: { status: 'active' } } }),
......
...@@ -157,7 +157,7 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -157,7 +157,7 @@ describe(Support.getTestDialectTeaser('Include'), function() {
); );
}) })
.spread(function(a, b) { .spread(function(a, b) {
return a.addB(b, {name : 'Foobar'}); return a.addB(b, { through: {name : 'Foobar'}});
}) })
.then(function() { .then(function() {
return A.find({ return A.find({
......
...@@ -770,12 +770,12 @@ describe(Support.getTestDialectTeaser('Include'), function() { ...@@ -770,12 +770,12 @@ describe(Support.getTestDialectTeaser('Include'), function() {
}) })
}).then(function (results) { }).then(function (results) {
return Promise.join( return Promise.join(
results.products[0].addTag(results.tags[0], {priority: 1}), results.products[0].addTag(results.tags[0], { through: {priority: 1}}),
results.products[0].addTag(results.tags[1], {priority: 2}), results.products[0].addTag(results.tags[1], { through: {priority: 2}}),
results.products[1].addTag(results.tags[1], {priority: 1}), results.products[1].addTag(results.tags[1], { through: {priority: 1}}),
results.products[2].addTag(results.tags[0], {priority: 3}), results.products[2].addTag(results.tags[0], { through: {priority: 3}}),
results.products[2].addTag(results.tags[1], {priority: 1}), results.products[2].addTag(results.tags[1], { through: {priority: 1}}),
results.products[2].addTag(results.tags[2], {priority: 2}) results.products[2].addTag(results.tags[2], { through: {priority: 2}})
); );
}).then(function () { }).then(function () {
return Product.findAll({ return Product.findAll({
......
...@@ -518,12 +518,12 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), function() { ...@@ -518,12 +518,12 @@ describe(Support.getTestDialectTeaser('Includes with schemas'), function() {
]); ]);
}).spread(function(products, tags) { }).spread(function(products, tags) {
return Promise.all([ return Promise.all([
products[0].addTag(tags[0], {priority: 1}), products[0].addTag(tags[0], { through: {priority: 1}}),
products[0].addTag(tags[1], {priority: 2}), products[0].addTag(tags[1], { through: {priority: 2}}),
products[1].addTag(tags[1], {priority: 1}), products[1].addTag(tags[1], { through: {priority: 1}}),
products[2].addTag(tags[0], {priority: 3}), products[2].addTag(tags[0], { through: {priority: 3}}),
products[2].addTag(tags[1], {priority: 1}), products[2].addTag(tags[1], { through: {priority: 1}}),
products[2].addTag(tags[2], {priority: 2}) products[2].addTag(tags[2], { through: {priority: 2}})
]); ]);
}).spread(function() { }).spread(function() {
return Product.findAll({ return Product.findAll({
......
...@@ -51,7 +51,7 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -51,7 +51,7 @@ describe(Support.getTestDialectTeaser('Model'), function() {
self.Student.create({no: 1, name: 'ryan'}), self.Student.create({no: 1, name: 'ryan'}),
self.Course.create({no: 100, name: 'history'}) self.Course.create({no: 100, name: 'history'})
).spread(function(student, course) { ).spread(function(student, course) {
return student.addCourse(course, {score: 98, test_value: 1000}); return student.addCourse(course, { through: {score: 98, test_value: 1000}});
}).then(function() { }).then(function() {
expect(self.callCount).to.equal(1); expect(self.callCount).to.equal(1);
return self.Score.find({ where: { StudentId: 1, CourseId: 100 } }).then(function(score) { return self.Score.find({ where: { StudentId: 1, CourseId: 100 } }).then(function(score) {
......
...@@ -1121,10 +1121,10 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1121,10 +1121,10 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}); });
return self.sequelize.Promise.all([ return self.sequelize.Promise.all([
self.england.addIndustry(self.energy, {numYears: 20}), self.england.addIndustry(self.energy, { through: { numYears: 20 }}),
self.england.addIndustry(self.media, {numYears: 40}), self.england.addIndustry(self.media, { through: { numYears: 40 }}),
self.france.addIndustry(self.media, {numYears: 80}), self.france.addIndustry(self.media, { through: { numYears: 80 }}),
self.korea.addIndustry(self.tech, {numYears: 30}) self.korea.addIndustry(self.tech, { through: { numYears: 30 }})
]); ]);
}); });
}); });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!