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

Commit cdf00c41 by tornillo

Association mixins refactoring

Fix es6 syntax, docs and changelog
1 parent 95d3a571
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- [FIXED] Issue with query generation in MSSQL, an identifier was not escaped [#6686] (https://github.com/sequelize/sequelize/pull/6686) - [FIXED] Issue with query generation in MSSQL, an identifier was not escaped [#6686] (https://github.com/sequelize/sequelize/pull/6686)
- [FIXED] GroupedLimit when foreignKey has a field alias - [FIXED] GroupedLimit when foreignKey has a field alias
- [FIXED] groupedLimit.through.where support - [FIXED] groupedLimit.through.where support
- [FIXED] Issue with overrriding custom methods with association mixins (all association methods are now exposed) [#6682](https://github.com/sequelize/sequelize/issues/6682)
# 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
......
...@@ -140,14 +140,9 @@ class BelongsTo extends Association { ...@@ -140,14 +140,9 @@ class BelongsTo extends Association {
} }
mixin(obj) { mixin(obj) {
const association = this; const methods = ['get', 'set', 'create'];
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
association.injectSetter(obj); Helpers.mixinMethods(this, obj, methods);
association.injectCreator(obj);
} }
get(instances, options) { get(instances, options) {
...@@ -206,57 +201,47 @@ class BelongsTo extends Association { ...@@ -206,57 +201,47 @@ class BelongsTo extends Association {
return result; return result;
}); });
} }
return Target.findOne(options); return Target.findOne(options);
} }
// Add setAssociation method to the prototype of the model instance set(sourceInstance, associatedInstance, options) {
injectSetter(instancePrototype) {
const association = this; const association = this;
instancePrototype[this.accessors.set] = function(associatedInstance, options) { options = options || {};
options = options || {};
let value = associatedInstance; let value = associatedInstance;
if (associatedInstance instanceof association.target) { if (associatedInstance instanceof association.target) {
value = associatedInstance[association.targetKey]; value = associatedInstance[association.targetKey];
} }
this.set(association.foreignKey, value);
if (options.save === false) return;
options = _.extend({ sourceInstance.set(association.foreignKey, value);
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}, options);
if (options.save === false) return;
// passes the changed field to save, so only that field get updated. options = _.extend({
return this.save(options); fields: [association.foreignKey],
}; allowNull: [association.foreignKey],
association: true
}, options);
return this; // passes the changed field to save, so only that field get updated.
return sourceInstance.save(options);
} }
// Add createAssociation method to the prototype of the model instance create(sourceInstance, values, fieldsOrOptions) {
injectCreator(instancePrototype) {
const association = this; const association = this;
instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) { const options = {};
const options = {};
if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction;
}
options.logging = (fieldsOrOptions || {}).logging;
return association.target.create(values, fieldsOrOptions).then(newAssociatedObject => if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
this[association.accessors.set](newAssociatedObject, options) options.transaction = fieldsOrOptions.transaction;
); }
}; options.logging = (fieldsOrOptions || {}).logging;
return this; return association.target.create(values, fieldsOrOptions).then(newAssociatedObject =>
sourceInstance[association.accessors.set](newAssociatedObject, options)
);
} }
} }
......
...@@ -233,37 +233,15 @@ class HasMany extends Association { ...@@ -233,37 +233,15 @@ class HasMany extends Association {
} }
mixin(obj) { mixin(obj) {
const association = this; const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
const aliases = {
obj[this.accessors.get] = function(options) { hasSingle: 'has',
return association.get(this, options); hasAll: 'has',
}; addMultiple: 'add',
removeMultiple: 'remove'
if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
return association.count(this, options);
};
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
return association.has(this, instances, options);
}; };
obj[this.accessors.set] = function(instances, options) { Helpers.mixinMethods(this, obj, methods, aliases);
return association.set(this, instances, options);
};
obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
return association.add(this, instances, options);
};
obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
return association.remove(this, instances, options);
};
obj[this.accessors.create] = function(values, options) {
return association.create(this, values, options);
};
} }
get(instances, options) { get(instances, options) {
...@@ -351,7 +329,7 @@ class HasMany extends Association { ...@@ -351,7 +329,7 @@ class HasMany extends Association {
options.raw = true; options.raw = true;
options.plain = true; options.plain = true;
return this.get(instance, options).then(result => parseInt(result.count, 10)); return association.get(instance, options).then(result => parseInt(result.count, 10));
} }
has(sourceInstance, targetInstances, options) { has(sourceInstance, targetInstances, options) {
...@@ -384,7 +362,7 @@ class HasMany extends Association { ...@@ -384,7 +362,7 @@ class HasMany extends Association {
] ]
}; };
return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length); return association.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
} }
set(sourceInstance, targetInstances, options) { set(sourceInstance, targetInstances, options) {
......
...@@ -138,14 +138,9 @@ class HasOne extends Association { ...@@ -138,14 +138,9 @@ class HasOne extends Association {
} }
mixin(obj) { mixin(obj) {
const association = this; const methods = ['get', 'set', 'create'];
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
association.injectSetter(obj); Helpers.mixinMethods(this, obj, methods);
association.injectCreator(obj);
} }
get(instances, options) { get(instances, options) {
...@@ -206,71 +201,70 @@ class HasOne extends Association { ...@@ -206,71 +201,70 @@ class HasOne extends Association {
return Target.findOne(options); return Target.findOne(options);
} }
injectSetter(instancePrototype) { set(sourceInstance, associatedInstance, options) {
const association = this; const association = this;
instancePrototype[this.accessors.set] = function(associatedInstance, options) { let alreadyAssociated;
let alreadyAssociated;
options = _.assign({}, options, { options = _.assign({}, options, {
scope: false scope: false
}); });
return this[association.accessors.get](options).then(oldInstance => {
// TODO Use equals method once #5605 is resolved return sourceInstance[association.accessors.get](options).then(oldInstance => {
alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, attribute => // TODO Use equals method once #5605 is resolved
oldInstance.get(attribute, {raw: true}) === (associatedInstance.get ? associatedInstance.get(attribute, {raw: true}) : associatedInstance) alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, attribute =>
); oldInstance.get(attribute, {raw: true}) === (associatedInstance.get ? associatedInstance.get(attribute, {raw: true}) : associatedInstance)
);
if (oldInstance && !alreadyAssociated) {
oldInstance[association.foreignKey] = null; if (oldInstance && !alreadyAssociated) {
return oldInstance.save(_.extend({}, options, { oldInstance[association.foreignKey] = null;
fields: [association.foreignKey], return oldInstance.save(_.extend({}, options, {
allowNull: [association.foreignKey], fields: [association.foreignKey],
association: true allowNull: [association.foreignKey],
})); association: true
} }));
}).then(() => { }
if (associatedInstance && !alreadyAssociated) { }).then(() => {
if (!(associatedInstance instanceof association.target)) { if (associatedInstance && !alreadyAssociated) {
const tmpInstance = {}; if (!(associatedInstance instanceof association.target)) {
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance; const tmpInstance = {};
associatedInstance = association.target.build(tmpInstance, { tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
isNewRecord: false associatedInstance = association.target.build(tmpInstance, {
}); isNewRecord: false
} });
_.assign(associatedInstance, association.scope);
associatedInstance.set(association.foreignKey, this.get(association.sourceIdentifier));
return associatedInstance.save(options);
} }
return null;
});
};
return this; _.assign(associatedInstance, association.scope);
associatedInstance.set(association.foreignKey, sourceInstance.get(association.sourceIdentifier));
return associatedInstance.save(options);
}
return null;
});
} }
injectCreator(instancePrototype) { create(sourceInstance, values, options) {
const association = this; const association = this;
instancePrototype[this.accessors.create] = function(values, options) { values = values || {};
values = values || {}; options = options || {};
options = options || {};
if (association.scope) { if (association.scope) {
for (const attribute of Object.keys(association.scope)) { for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute]; values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute); if (options.fields) {
options.fields.push(attribute);
} }
} }
}
values[association.foreignKey] = this.get(association.sourceIdentifier); values[association.foreignKey] = sourceInstance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.foreignKey); if (options.fields) {
return association.target.create(values, options); options.fields.push(association.foreignKey);
}; }
return this; return association.target.create(values, options);
} }
} }
......
...@@ -44,3 +44,30 @@ function addForeignKeyConstraints(newAttribute, source, target, options, key) { ...@@ -44,3 +44,30 @@ function addForeignKeyConstraints(newAttribute, source, target, options, key) {
} }
} }
exports.addForeignKeyConstraints = addForeignKeyConstraints; exports.addForeignKeyConstraints = addForeignKeyConstraints;
/**
* Mixin (inject) association methods to model prototype
*
* @param {Object} Association instance
* @param {Object} Model prototype
* @param {Array} Method names to inject
* @param {Object} Mapping between model and association method names
*/
function mixinMethods(association, obj, methods, aliases) {
aliases = aliases || {};
for (const method of methods) {
// don't override custom methods
if (!obj[association.accessors[method]]) {
const realMethod = aliases[method] || method;
obj[association.accessors[method]] = function() {
const instance = this;
const args = [instance].concat(Array.from(arguments));
return association[realMethod].apply(association, args);
};
}
}
}
exports.mixinMethods = mixinMethods;
...@@ -201,11 +201,10 @@ const Mixin = { ...@@ -201,11 +201,10 @@ const Mixin = {
// the id is in the foreign table or in a connecting table // the id is in the foreign table or in a connecting table
const association = new BelongsToMany(sourceModel, targetModel, options); const association = new BelongsToMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes(); sourceModel.associations[association.associationAccessor] = association;
association.injectGetter(sourceModel.prototype); association.injectAttributes();
association.injectSetter(sourceModel.prototype); association.mixin(sourceModel.prototype);
association.injectCreator(sourceModel.prototype);
return association; return association;
}, },
...@@ -241,15 +240,10 @@ function singleLinked(Type) { ...@@ -241,15 +240,10 @@ function singleLinked(Type) {
// the id is in the foreign table // the id is in the foreign table
const association = new Type(source, target, _.extend(options, source.options)); const association = new Type(source, target, _.extend(options, source.options));
source.associations[association.associationAccessor] = association.injectAttributes(); source.associations[association.associationAccessor] = association;
if (association.mixin) { association.injectAttributes();
association.mixin(source.prototype); association.mixin(source.prototype);
} else {
association.injectGetter(source.prototype);
association.injectSetter(source.prototype);
association.injectCreator(source.prototype);
}
return association; return association;
}; };
......
...@@ -58,6 +58,39 @@ describe(Support.getTestDialectTeaser('belongsToMany'), function() { ...@@ -58,6 +58,39 @@ describe(Support.getTestDialectTeaser('belongsToMany'), function() {
expect(AB.options.validate).to.deep.equal({}); expect(AB.options.validate).to.deep.equal({});
}); });
it('should not override custom methods with association mixin', function(){
const methods = {
getTasks: 'get',
countTasks: 'count',
hasTask: 'has',
hasTasks: 'has',
setTasks: 'set',
addTask: 'add',
addTasks: 'add',
removeTask: 'remove',
removeTasks: 'remove',
createTask: 'create',
};
const User = current.define('User');
const Task = current.define('Task');
current.Utils._.each(methods, (alias, method) => {
User.prototype[method] = function () {
const realMethod = this.constructor.associations.task[alias];
expect(realMethod).to.be.a('function');
return realMethod;
};
});
User.belongsToMany(Task, { through: 'UserTasks', as: 'task' });
const user = User.build();
current.Utils._.each(methods, (alias, method) => {
expect(user[method]()).to.be.a('function');
});
});
describe('timestamps', function () { describe('timestamps', function () {
it('follows the global timestamps true option', function () { it('follows the global timestamps true option', function () {
var User = current.define('User', {}) var User = current.define('User', {})
......
'use strict';
/* jshint -W030 */
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/../support')
, current = Support.sequelize;
describe(Support.getTestDialectTeaser('belongsTo'), function() {
it('should not override custom methods with association mixin', function(){
const methods = {
getTask : 'get',
setTask: 'set',
createTask: 'create'
};
const User = current.define('User');
const Task = current.define('Task');
current.Utils._.each(methods, (alias, method) => {
User.prototype[method] = function () {
const realMethod = this.constructor.associations.task[alias];
expect(realMethod).to.be.a('function');
return realMethod;
};
});
User.belongsTo(Task, { as: 'task' });
const user = User.build();
current.Utils._.each(methods, (alias, method) => {
expect(user[method]()).to.be.a('function');
});
});
});
...@@ -84,6 +84,37 @@ describe(Support.getTestDialectTeaser('hasMany'), function() { ...@@ -84,6 +84,37 @@ describe(Support.getTestDialectTeaser('hasMany'), function() {
expect(obj[association.accessors.hasAll]).to.be.an('function'); expect(obj[association.accessors.hasAll]).to.be.an('function');
expect(obj[association.accessors.count]).to.be.an('function'); expect(obj[association.accessors.count]).to.be.an('function');
}); });
it('should not override custom methods', function(){
const methods = {
getTasks: 'get',
countTasks: 'count',
hasTask: 'has',
hasTasks: 'has',
setTasks: 'set',
addTask: 'add',
addTasks: 'add',
removeTask: 'remove',
removeTasks: 'remove',
createTask: 'create',
};
current.Utils._.each(methods, (alias, method) => {
User.prototype[method] = function () {
const realMethod = this.constructor.associations.task[alias];
expect(realMethod).to.be.a('function');
return realMethod;
};
});
User.hasMany(Task, { as: 'task' });
const user = User.build();
current.Utils._.each(methods, (alias, method) => {
expect(user[method]()).to.be.a('function');
});
});
}); });
describe('get', function () { describe('get', function () {
......
...@@ -18,4 +18,30 @@ describe(Support.getTestDialectTeaser('hasOne'), function() { ...@@ -18,4 +18,30 @@ describe(Support.getTestDialectTeaser('hasOne'), function() {
User.hasOne(Task, {as : 'Shabda'}); User.hasOne(Task, {as : 'Shabda'});
expect(Task.attributes.ShabdaId).not.to.be.empty; expect(Task.attributes.ShabdaId).not.to.be.empty;
}); });
it('should not override custom methods with association mixin', function(){
const methods = {
getTask : 'get',
setTask: 'set',
createTask: 'create'
};
const User = current.define('User');
const Task = current.define('Task');
current.Utils._.each(methods, (alias, method) => {
User.prototype[method] = function () {
const realMethod = this.constructor.associations.task[alias];
expect(realMethod).to.be.a('function');
return realMethod;
};
});
User.hasOne(Task, { as: 'task' });
const user = User.build();
current.Utils._.each(methods, (alias, method) => {
expect(user[method]()).to.be.a('function');
});
});
}); });
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!