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

Commit 5941bfe7 by Sushant Committed by GitHub

change(model): new options.underscored implementation (#9304)

1 parent 0cbb7b9f
...@@ -530,8 +530,8 @@ const Bar = sequelize.define('bar', { /* bla */ }, { ...@@ -530,8 +530,8 @@ const Bar = sequelize.define('bar', { /* bla */ }, {
// timestamps are enabled // timestamps are enabled
paranoid: true, paranoid: true,
// don't use camelcase for automatically added attributes but underscore style // Will automatically set field option for all attributes to snake cased name.
// so updatedAt will be updated_at // Does not override attribute with field option already defined
underscored: true, underscored: true,
// disable the modification of table names; By default, sequelize will automatically // disable the modification of table names; By default, sequelize will automatically
......
...@@ -80,20 +80,22 @@ const AssociationError = require('./../errors').AssociationError; ...@@ -80,20 +80,22 @@ const AssociationError = require('./../errors').AssociationError;
* Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would create a cyclic dependency, and sequelize would not know which table to create first, since user depends on picture, and picture depends on user. These kinds of problems are detected by sequelize before the models are synced to the database, and you will get an error along the lines of `Error: Cyclic dependency found. 'users' is dependent of itself`. If you encounter this, you should either disable some constraints, or rethink your associations completely. * Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would create a cyclic dependency, and sequelize would not know which table to create first, since user depends on picture, and picture depends on user. These kinds of problems are detected by sequelize before the models are synced to the database, and you will get an error along the lines of `Error: Cyclic dependency found. 'users' is dependent of itself`. If you encounter this, you should either disable some constraints, or rethink your associations completely.
*/ */
class Association { class Association {
constructor(source, target, options) { constructor(source, target, options = {}) {
options = options || {};
/** /**
* @type {Model} * @type {Model}
*/ */
this.source = source; this.source = source;
/** /**
* @type {Model} * @type {Model}
*/ */
this.target = target; this.target = target;
this.options = options; this.options = options;
this.scope = options.scope; this.scope = options.scope;
this.isSelfAssociation = this.source === this.target; this.isSelfAssociation = this.source === this.target;
this.as = options.as; this.as = options.as;
/** /**
* The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany` * The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany`
* @type {string} * @type {string}
...@@ -106,22 +108,29 @@ class Association { ...@@ -106,22 +108,29 @@ class Association {
); );
} }
} }
// Normalize input - may be array or single obj, instance or primary key - convert it to an array of built objects
toInstanceArray(objs) { /**
if (!Array.isArray(objs)) { * Normalize input
objs = [objs]; *
* @param input {Any}, it may be array or single obj, instance or primary key
*
* @returns <Array>, built objects
*/
toInstanceArray(input) {
if (!Array.isArray(input)) {
input = [input];
} }
return objs.map(function(obj) {
if (!(obj instanceof this.target)) { return input.map(element => {
const tmpInstance = {}; if (element instanceof this.target) return element;
tmpInstance[this.target.primaryKeyAttribute] = obj;
return this.target.build(tmpInstance, { const tmpInstance = {};
isNewRecord: false tmpInstance[this.target.primaryKeyAttribute] = element;
});
} return this.target.build(tmpInstance, { isNewRecord: false });
return obj; });
}, this);
} }
inspect() { inspect() {
return this.as; return this.as;
} }
......
...@@ -121,12 +121,11 @@ class BelongsToMany extends Association { ...@@ -121,12 +121,11 @@ class BelongsToMany extends Association {
} }
this.foreignKeyAttribute = {}; this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelizeIf( this.foreignKey = this.options.foreignKey || Utils.camelize(
[ [
Utils.underscoredIf(this.source.options.name.singular, this.source.options.underscored), this.source.options.name.singular,
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_')
!this.source.options.underscored
); );
} }
...@@ -139,17 +138,11 @@ class BelongsToMany extends Association { ...@@ -139,17 +138,11 @@ class BelongsToMany extends Association {
} }
this.otherKeyAttribute = {}; this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelizeIf( this.otherKey = this.options.otherKey || Utils.camelize(
[ [
Utils.underscoredIf( this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.isSelfAssociation ?
Utils.singularize(this.as) :
this.target.options.name.singular,
this.target.options.underscored
),
this.target.primaryKeyAttribute this.target.primaryKeyAttribute
].join('_'), ].join('_')
!this.target.options.underscored
); );
} }
...@@ -188,13 +181,13 @@ class BelongsToMany extends Association { ...@@ -188,13 +181,13 @@ class BelongsToMany extends Association {
this.otherKey = this.paired.foreignKey; this.otherKey = this.paired.foreignKey;
} }
if (this.paired.otherKeyDefault) { if (this.paired.otherKeyDefault) {
// If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey // If paired otherKey was inferred we should make sure to clean it up
// before adding a new one that matches the foreignKey
if (this.paired.otherKey !== this.foreignKey) { if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey]; delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.otherKey = this.foreignKey;
this.paired._injectAttributes();
} }
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
} }
} }
...@@ -226,8 +219,7 @@ class BelongsToMany extends Association { ...@@ -226,8 +219,7 @@ class BelongsToMany extends Association {
// the id is in the target table // the id is in the target table
// or in an extra table which connects two tables // or in an extra table which connects two tables
injectAttributes() { _injectAttributes() {
this.identifier = this.foreignKey; this.identifier = this.foreignKey;
this.foreignIdentifier = this.otherKey; this.foreignIdentifier = this.otherKey;
...@@ -302,6 +294,8 @@ class BelongsToMany extends Association { ...@@ -302,6 +294,8 @@ class BelongsToMany extends Association {
this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute); this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute); this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.through.model.refreshAttributes();
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey; this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey; this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
...@@ -309,8 +303,6 @@ class BelongsToMany extends Association { ...@@ -309,8 +303,6 @@ class BelongsToMany extends Association {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey; this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
} }
this.through.model.refreshAttributes();
this.toSource = new BelongsTo(this.through.model, this.source, { this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey foreignKey: this.foreignKey
}); });
......
...@@ -41,17 +41,15 @@ class BelongsTo extends Association { ...@@ -41,17 +41,15 @@ class BelongsTo extends Association {
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelize(
[ [
Utils.underscoredIf(this.as, this.source.options.underscored), this.as,
this.target.primaryKeyAttribute this.target.primaryKeyAttribute
].join('_'), ].join('_')
!this.source.options.underscored
); );
} }
this.identifier = this.foreignKey; this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) { if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier; this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
} }
...@@ -75,7 +73,7 @@ class BelongsTo extends Association { ...@@ -75,7 +73,7 @@ class BelongsTo extends Association {
} }
// the id is in the source table // the id is in the source table
injectAttributes() { _injectAttributes() {
const newAttributes = {}; const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, { newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
...@@ -92,10 +90,10 @@ class BelongsTo extends Association { ...@@ -92,10 +90,10 @@ class BelongsTo extends Association {
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField); Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes); Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
this.source.refreshAttributes(); this.source.refreshAttributes();
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
......
...@@ -54,8 +54,8 @@ class HasMany extends Association { ...@@ -54,8 +54,8 @@ class HasMany extends Association {
} }
/* /*
* Foreign key setup * Foreign key setup
*/ */
if (_.isObject(this.options.foreignKey)) { if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
...@@ -64,12 +64,11 @@ class HasMany extends Association { ...@@ -64,12 +64,11 @@ class HasMany extends Association {
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelize(
[ [
Utils.underscoredIf(this.source.options.name.singular, this.source.options.underscored), this.source.options.name.singular,
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_')
!this.source.options.underscored
); );
} }
...@@ -78,25 +77,25 @@ class HasMany extends Association { ...@@ -78,25 +77,25 @@ class HasMany extends Association {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
} }
/*
* Source key setup
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute; this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
if (this.target.rawAttributes[this.sourceKey]) { this.sourceIdentifier = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyField = this.sourceKey;
}
if (this.source.fieldRawAttributesMap[this.sourceKey]) { if (this.source.rawAttributes[this.sourceKey]) {
this.sourceKeyAttribute = this.source.fieldRawAttributesMap[this.sourceKey].fieldName; this.sourceKeyAttribute = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else { } else {
this.sourceKeyAttribute = this.source.primaryKeyAttribute; this.sourceKeyAttribute = this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.primaryKeyField;
} }
this.sourceIdentifier = this.sourceKey;
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it // Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
const plural = Utils.uppercaseFirst(this.options.name.plural); const plural = Utils.uppercaseFirst(this.options.name.plural);
const singular = Utils.uppercaseFirst(this.options.name.singular); const singular = Utils.uppercaseFirst(this.options.name.singular);
this.associationAccessor = this.as;
this.accessors = { this.accessors = {
get: 'get' + plural, get: 'get' + plural,
set: 'set' + plural, set: 'set' + plural,
...@@ -113,9 +112,11 @@ class HasMany extends Association { ...@@ -113,9 +112,11 @@ class HasMany extends Association {
// the id is in the target table // the id is in the target table
// or in an extra table which connects two tables // or in an extra table which connects two tables
injectAttributes() { _injectAttributes() {
const newAttributes = {}; const newAttributes = {};
const constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
const constraintOptions = _.clone(this.options);
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, { newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].type, type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].type,
allowNull: true allowNull: true
...@@ -126,15 +127,17 @@ class HasMany extends Association { ...@@ -126,15 +127,17 @@ class HasMany extends Association {
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE'); constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE'; constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions, this.sourceKeyField); Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes); Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.target.refreshAttributes(); this.target.refreshAttributes();
this.source.refreshAttributes(); this.source.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
......
...@@ -40,12 +40,11 @@ class HasOne extends Association { ...@@ -40,12 +40,11 @@ class HasOne extends Association {
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelize(
[ [
Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored), Utils.singularize(this.options.as || this.source.name),
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_')
!this.source.options.underscored
); );
} }
...@@ -71,7 +70,7 @@ class HasOne extends Association { ...@@ -71,7 +70,7 @@ class HasOne extends Association {
} }
// the id is in the target table // the id is in the target table
injectAttributes() { _injectAttributes() {
const newAttributes = {}; const newAttributes = {};
const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type; const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
...@@ -79,9 +78,6 @@ class HasOne extends Association { ...@@ -79,9 +78,6 @@ class HasOne extends Association {
type: this.options.keyType || keyType, type: this.options.keyType || keyType,
allowNull: true allowNull: true
}); });
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
if (this.options.constraints !== false) { if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey]; const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
...@@ -89,11 +85,13 @@ class HasOne extends Association { ...@@ -89,11 +85,13 @@ class HasOne extends Association {
this.options.onUpdate = this.options.onUpdate || 'CASCADE'; this.options.onUpdate = this.options.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options); Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
// Sync attributes and setters/getters to Model prototype
this.target.refreshAttributes(); this.target.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this); Helpers.checkNamingCollision(this);
return this; return this;
......
'use strict'; 'use strict';
const _ = require('lodash');
function checkNamingCollision(association) { function checkNamingCollision(association) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) { if (association.source.rawAttributes.hasOwnProperty(association.as)) {
throw new Error( throw new Error(
'Naming collision between attribute \'' + association.as + `Naming collision between attribute '${association.as}'` +
'\' and association \'' + association.as + '\' on model ' + association.source.name + ` and association '${association.as}' on model ${association.source.name}` +
'. To remedy this, change either foreignKey or as in your association definition' '. To remedy this, change either foreignKey or as in your association definition'
); );
} }
...@@ -15,14 +13,12 @@ exports.checkNamingCollision = checkNamingCollision; ...@@ -15,14 +13,12 @@ exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints(newAttribute, source, target, options, key) { function addForeignKeyConstraints(newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints` // FK constraints are opt-in: users must either set `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behaviour // on the association, or request an `onDelete` or `onUpdate` behavior
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) { if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach // Find primary keys: composite keys not supported with this approach
const primaryKeys = _.chain(source.rawAttributes).keys() const primaryKeys = Object.keys(source.primaryKeys)
.filter(key => source.rawAttributes[key].primaryKey) .map(primaryKeyAttribute => source.rawAttributes[primaryKeyAttribute].field || primaryKeyAttribute);
.map(key => source.rawAttributes[key].field || key).value();
if (primaryKeys.length === 1) { if (primaryKeys.length === 1) {
if (source._schema) { if (source._schema) {
......
...@@ -7,16 +7,21 @@ const HasMany = require('./has-many'); ...@@ -7,16 +7,21 @@ const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many'); const BelongsToMany = require('./belongs-to-many');
const BelongsTo = require('./belongs-to'); const BelongsTo = require('./belongs-to');
function isModel(model, sequelize) {
return model
&& model.prototype
&& model.prototype instanceof sequelize.Model;
}
const Mixin = { const Mixin = {
hasMany(target, options) { // testhint options:none hasMany(target, options = {}) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) { if (!isModel(target, this.sequelize)) {
throw new Error(this.name + '.hasMany called with something that\'s not a subclass of Sequelize.Model'); throw new Error(`${this.name}.hasMany called with something that's not a subclass of Sequelize.Model`);
} }
const source = this; const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option) // Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks; options.useHooks = options.hooks;
...@@ -26,32 +31,31 @@ const Mixin = { ...@@ -26,32 +31,31 @@ const Mixin = {
const association = new HasMany(source, target, options); const association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association; source.associations[association.associationAccessor] = association;
association.injectAttributes(); association._injectAttributes();
association.mixin(source.prototype); association.mixin(source.prototype);
return association; return association;
}, },
belongsToMany(targetModel, options) { // testhint options:none belongsToMany(target, options = {}) { // testhint options:none
if (!targetModel || !targetModel.prototype || !(targetModel.prototype instanceof this.sequelize.Model)) { if (!isModel(target, this.sequelize)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not a subclass of Sequelize.Model'); throw new Error(`${this.name}.belongsToMany called with something that's not a subclass of Sequelize.Model`);
} }
const sourceModel = this; const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option) // Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks; options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps; options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = _.extend(options, _.omit(sourceModel.options, ['hooks', 'timestamps', 'scopes', 'defaultScope'])); options = _.extend(options, _.omit(source.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
// 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(source, target, options);
sourceModel.associations[association.associationAccessor] = association; source.associations[association.associationAccessor] = association;
association.injectAttributes(); association._injectAttributes();
association.mixin(sourceModel.prototype); association.mixin(source.prototype);
return association; return association;
}, },
...@@ -76,15 +80,14 @@ const Mixin = { ...@@ -76,15 +80,14 @@ const Mixin = {
// The logic for hasOne and belongsTo is exactly the same // The logic for hasOne and belongsTo is exactly the same
function singleLinked(Type) { function singleLinked(Type) {
return function(target, options) { // testhint options:none return function(target, options = {}) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) { if (!isModel(target, this.sequelize)) {
throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not a subclass of Sequelize.Model'); throw new Error(`${this.name}.${Utils.lowercaseFirst(Type.name)} called with something that's not a subclass of Sequelize.Model`);
} }
const source = this; const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option) // Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks; options.useHooks = options.hooks;
...@@ -92,7 +95,7 @@ function singleLinked(Type) { ...@@ -92,7 +95,7 @@ function singleLinked(Type) {
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; source.associations[association.associationAccessor] = association;
association.injectAttributes(); association._injectAttributes();
association.mixin(source.prototype); association.mixin(source.prototype);
return association; return association;
...@@ -100,7 +103,6 @@ function singleLinked(Type) { ...@@ -100,7 +103,6 @@ function singleLinked(Type) {
} }
Mixin.hasOne = singleLinked(HasOne); Mixin.hasOne = singleLinked(HasOne);
Mixin.belongsTo = singleLinked(BelongsTo); Mixin.belongsTo = singleLinked(BelongsTo);
module.exports = Mixin; module.exports = Mixin;
......
...@@ -682,8 +682,7 @@ class Model { ...@@ -682,8 +682,7 @@ class Model {
* @param {Boolean} [options.omitNull] Don't persist null values. This means that all columns with null values will not be saved * @param {Boolean} [options.omitNull] Don't persist null values. This means that all columns with null values will not be saved
* @param {Boolean} [options.timestamps=true] Adds createdAt and updatedAt timestamps to the model. * @param {Boolean} [options.timestamps=true] Adds createdAt and updatedAt timestamps to the model.
* @param {Boolean} [options.paranoid=false] Calling `destroy` will not delete the model, but instead set a `deletedAt` timestamp if this is true. Needs `timestamps=true` to work * @param {Boolean} [options.paranoid=false] Calling `destroy` will not delete the model, but instead set a `deletedAt` timestamp if this is true. Needs `timestamps=true` to work
* @param {Boolean} [options.underscored=false] Converts all camelCased columns to underscored if true. Will not affect timestamp fields named explicitly by model options and will not affect fields with explicitly set `field` option * @param {Boolean} [options.underscored=false] Add underscored field to all attributes, this covers user defined attributes, timestamps and foreign keys. Will not affect attributes with explicitly set `field` option
* @param {Boolean} [options.underscoredAll=false] Converts camelCased model names to underscored table names if true. Will not change model name if freezeTableName is set to true
* @param {Boolean} [options.freezeTableName=false] If freezeTableName is true, sequelize will not try to alter the model name to get the table name. Otherwise, the model name will be pluralized * @param {Boolean} [options.freezeTableName=false] If freezeTableName is true, sequelize will not try to alter the model name to get the table name. Otherwise, the model name will be pluralized
* @param {Object} [options.name] An object with two attributes, `singular` and `plural`, which are used when this model is associated to others. * @param {Object} [options.name] An object with two attributes, `singular` and `plural`, which are used when this model is associated to others.
* @param {String} [options.name.singular=Utils.singularize(modelName)] * @param {String} [options.name.singular=Utils.singularize(modelName)]
...@@ -695,9 +694,9 @@ class Model { ...@@ -695,9 +694,9 @@ class Model {
* @param {Boolean} [options.indexes[].unique=false] Should the index by unique? Can also be triggered by setting type to `UNIQUE` * @param {Boolean} [options.indexes[].unique=false] Should the index by unique? Can also be triggered by setting type to `UNIQUE`
* @param {Boolean} [options.indexes[].concurrently=false] PostgreSQL will build the index without taking any write locks. Postgres only * @param {Boolean} [options.indexes[].concurrently=false] PostgreSQL will build the index without taking any write locks. Postgres only
* @param {Array<String|Object>} [options.indexes[].fields] An array of the fields to index. Each field can either be a string containing the name of the field, a sequelize object (e.g `sequelize.fn`), or an object with the following attributes: `attribute` (field name), `length` (create a prefix index of length chars), `order` (the direction the column should be sorted in), `collate` (the collation (sort order) for the column) * @param {Array<String|Object>} [options.indexes[].fields] An array of the fields to index. Each field can either be a string containing the name of the field, a sequelize object (e.g `sequelize.fn`), or an object with the following attributes: `attribute` (field name), `length` (create a prefix index of length chars), `order` (the direction the column should be sorted in), `collate` (the collation (sort order) for the column)
* @param {String|Boolean} [options.createdAt] Override the name of the createdAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting. * @param {String|Boolean} [options.createdAt] Override the name of the createdAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String|Boolean} [options.updatedAt] Override the name of the updatedAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting. * @param {String|Boolean} [options.updatedAt] Override the name of the updatedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String|Boolean} [options.deletedAt] Override the name of the deletedAt column if a string is provided, or disable it if false. Timestamps must be true. Not affected by underscored setting. * @param {String|Boolean} [options.deletedAt] Override the name of the deletedAt attribute if a string is provided, or disable it if false. Timestamps must be true. Underscored field will be set with underscored setting.
* @param {String} [options.tableName] Defaults to pluralized model name, unless freezeTableName is true, in which case it uses model name verbatim * @param {String} [options.tableName] Defaults to pluralized model name, unless freezeTableName is true, in which case it uses model name verbatim
* @param {String} [options.schema='public'] * @param {String} [options.schema='public']
* @param {String} [options.engine] * @param {String} [options.engine]
...@@ -747,7 +746,6 @@ class Model { ...@@ -747,7 +746,6 @@ class Model {
validate: {}, validate: {},
freezeTableName: false, freezeTableName: false,
underscored: false, underscored: false,
underscoredAll: false,
paranoid: false, paranoid: false,
rejectOnEmpty: false, rejectOnEmpty: false,
whereCollection: null, whereCollection: null,
...@@ -766,10 +764,10 @@ class Model { ...@@ -766,10 +764,10 @@ class Model {
this.associations = {}; this.associations = {};
this._setupHooks(options.hooks); this._setupHooks(options.hooks);
this.underscored = this.underscored || this.underscoredAll; this.underscored = this.options.underscored;
if (!this.options.tableName) { if (!this.options.tableName) {
this.tableName = this.options.freezeTableName ? this.name : Utils.underscoredIf(Utils.pluralize(this.name), this.options.underscoredAll); this.tableName = this.options.freezeTableName ? this.name : Utils.underscoredIf(Utils.pluralize(this.name), this.underscored);
} else { } else {
this.tableName = this.options.tableName; this.tableName = this.options.tableName;
} }
...@@ -789,7 +787,6 @@ class Model { ...@@ -789,7 +787,6 @@ class Model {
}); });
this.rawAttributes = _.mapValues(attributes, (attribute, name) => { this.rawAttributes = _.mapValues(attributes, (attribute, name) => {
attribute = this.sequelize.normalizeAttribute(attribute); attribute = this.sequelize.normalizeAttribute(attribute);
if (attribute.type === undefined) { if (attribute.type === undefined) {
...@@ -804,20 +801,22 @@ class Model { ...@@ -804,20 +801,22 @@ class Model {
}); });
this.primaryKeys = {}; this.primaryKeys = {};
this._timestampAttributes = {};
// Setup names of timestamp attributes // Setup names of timestamp attributes
this._timestampAttributes = {};
if (this.options.timestamps) { if (this.options.timestamps) {
if (this.options.createdAt !== false) { if (this.options.createdAt !== false) {
this._timestampAttributes.createdAt = this.options.createdAt || Utils.underscoredIf('createdAt', this.options.underscored); this._timestampAttributes.createdAt = this.options.createdAt || 'createdAt';
} }
if (this.options.updatedAt !== false) { if (this.options.updatedAt !== false) {
this._timestampAttributes.updatedAt = this.options.updatedAt || Utils.underscoredIf('updatedAt', this.options.underscored); this._timestampAttributes.updatedAt = this.options.updatedAt || 'updatedAt';
} }
if (this.options.paranoid && this.options.deletedAt !== false) { if (this.options.paranoid && this.options.deletedAt !== false) {
this._timestampAttributes.deletedAt = this.options.deletedAt || Utils.underscoredIf('deletedAt', this.options.underscored); this._timestampAttributes.deletedAt = this.options.deletedAt || 'deletedAt';
} }
} }
// Setup name for version attribute
if (this.options.version) { if (this.options.version) {
this._versionAttribute = typeof this.options.version === 'string' ? this.options.version : 'version'; this._versionAttribute = typeof this.options.version === 'string' ? this.options.version : 'version';
} }
...@@ -947,7 +946,7 @@ class Model { ...@@ -947,7 +946,7 @@ class Model {
definition._modelAttribute = true; definition._modelAttribute = true;
if (definition.field === undefined) { if (definition.field === undefined) {
definition.field = name; definition.field = Utils.underscoredIf(name, this.underscored);
} }
if (definition.primaryKey === true) { if (definition.primaryKey === true) {
......
...@@ -15,8 +15,6 @@ const chai = require('chai'), ...@@ -15,8 +15,6 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('BelongsToMany'), () => { describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
describe('getAssociations', () => { describe('getAssociations', () => {
beforeEach(function() { beforeEach(function() {
const self = this;
this.User = this.sequelize.define('User', { username: DataTypes.STRING }); this.User = this.sequelize.define('User', { username: DataTypes.STRING });
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN }); this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN });
...@@ -25,13 +23,13 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { ...@@ -25,13 +23,13 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
return this.sequelize.sync({ force: true }).then(() => { return this.sequelize.sync({ force: true }).then(() => {
return Promise.all([ return Promise.all([
self.User.create({ username: 'John'}), this.User.create({ username: 'John'}),
self.Task.create({ title: 'Get rich', active: true}), this.Task.create({ title: 'Get rich', active: true}),
self.Task.create({ title: 'Die trying', active: false}) this.Task.create({ title: 'Die trying', active: false})
]); ]);
}).spread((john, task1, task2) => { }).spread((john, task1, task2) => {
self.tasks = [task1, task2]; this.tasks = [task1, task2];
self.user = john; this.user = john;
return john.setTasks([task1, task2]); return john.setTasks([task1, task2]);
}); });
}); });
...@@ -1255,8 +1253,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { ...@@ -1255,8 +1253,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
const attributes = this.sequelize.model('user_places').rawAttributes; const attributes = this.sequelize.model('user_places').rawAttributes;
expect(attributes.place_id).to.be.ok; expect(attributes.PlaceId.field).to.equal('place_id');
expect(attributes.user_id).to.be.ok; expect(attributes.UserId.field).to.equal('user_id');
}); });
it('should infer otherKey from paired BTM relationship with a through string defined', function() { it('should infer otherKey from paired BTM relationship with a through string defined', function() {
...@@ -2276,10 +2274,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => { ...@@ -2276,10 +2274,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
PersonChildren = this.sequelize.define('PersonChildren', {}, {underscored: true}); PersonChildren = this.sequelize.define('PersonChildren', {}, {underscored: true});
Children = Person.belongsToMany(Person, { as: 'Children', through: PersonChildren}); Children = Person.belongsToMany(Person, { as: 'Children', through: PersonChildren});
expect(Children.foreignKey).to.equal('person_id'); expect(Children.foreignKey).to.equal('PersonId');
expect(Children.otherKey).to.equal('child_id'); expect(Children.otherKey).to.equal('ChildId');
expect(PersonChildren.rawAttributes[Children.foreignKey]).to.be.ok; expect(PersonChildren.rawAttributes[Children.foreignKey]).to.be.ok;
expect(PersonChildren.rawAttributes[Children.otherKey]).to.be.ok; expect(PersonChildren.rawAttributes[Children.otherKey]).to.be.ok;
expect(PersonChildren.rawAttributes[Children.foreignKey].field).to.equal('person_id');
expect(PersonChildren.rawAttributes[Children.otherKey].field).to.equal('child_id');
}); });
}); });
}); });
...@@ -12,7 +12,7 @@ const chai = require('chai'), ...@@ -12,7 +12,7 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('BelongsTo'), () => { describe(Support.getTestDialectTeaser('BelongsTo'), () => {
describe('Model.associations', () => { describe('Model.associations', () => {
it('should store all assocations when associting to the same table multiple times', function() { it('should store all associations when associating to the same table multiple times', function() {
const User = this.sequelize.define('User', {}), const User = this.sequelize.define('User', {}),
Group = this.sequelize.define('Group', {}); Group = this.sequelize.define('Group', {});
...@@ -20,7 +20,9 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -20,7 +20,9 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
Group.belongsTo(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' }); Group.belongsTo(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
Group.belongsTo(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' }); Group.belongsTo(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });
expect(Object.keys(Group.associations)).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']); expect(
Object.keys(Group.associations)
).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
}); });
}); });
...@@ -62,7 +64,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -62,7 +64,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
}); });
describe('getAssociation', () => { describe('getAssociation', () => {
if (current.dialect.supports.transactions) { if (current.dialect.supports.transactions) {
it('supports transactions', function() { it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => { return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
...@@ -452,13 +453,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -452,13 +453,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
}); });
describe('foreign key', () => { describe('foreign key', () => {
it('should lowercase foreign keys when using underscored', function() { it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }), const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true }); Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
User.belongsTo(Account); User.belongsTo(Account);
expect(User.rawAttributes.account_id).to.exist; expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('account_id');
}); });
it('should use model name when using camelcase', function() { it('should use model name when using camelcase', function() {
...@@ -468,6 +470,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -468,6 +470,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
User.belongsTo(Account); User.belongsTo(Account);
expect(User.rawAttributes.AccountId).to.exist; expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('AccountId');
}); });
it('should support specifying the field of a foreign key', function() { it('should support specifying the field of a foreign key', function() {
...@@ -497,15 +500,114 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -497,15 +500,114 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
return user.getAccount(); return user.getAccount();
}); });
}).then(user => { }).then(user => {
// the sql query should correctly look at task_id instead of taskId
expect(user).to.not.be.null; expect(user).to.not.be.null;
return User.findOne({ return User.findOne({
where: {username: 'foo'}, where: {username: 'foo'},
include: [Account] include: [Account]
}); });
}).then(task => { }).then(user => {
expect(task.Account).to.exist; // the sql query should correctly look at account_id instead of AccountId
expect(user.Account).to.exist;
});
});
it('should set foreignKey on foreign table', function() {
const Mail = this.sequelize.define('mail', {}, { timestamps: false });
const Entry = this.sequelize.define('entry', {}, { timestamps: false });
const User = this.sequelize.define('user', {}, { timestamps: false });
Entry.belongsTo(User, {
as: 'owner',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
Entry.belongsTo(Mail, {
as: 'mail',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
Mail.belongsToMany(User, {
as: 'recipients',
through: 'MailRecipients',
otherKey: {
name: 'recipientId',
allowNull: false
},
foreignKey: {
name: 'mailId',
allowNull: false
},
timestamps: false
}); });
Mail.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
User.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
return this.sequelize.sync({ force: true })
.then(() => User.create({}))
.then(() => Mail.create({}))
.then(mail =>
Entry.create({ mailId: mail.id, ownerId: 1 })
.then(() => Entry.create({ mailId: mail.id, ownerId: 1 }))
// set recipients
.then(() => mail.setRecipients([1]))
)
.then(() => Entry.findAndCount({
offset: 0,
limit: 10,
order: [['id', 'DESC']],
include: [
{
association: Entry.associations.mail,
include: [
{
association: Mail.associations.recipients,
through: {
where: {
recipientId: 1
}
},
required: true
}
],
required: true
}
]
})).then(result => {
expect(result.count).to.equal(2);
expect(result.rows[0].get({ plain: true })).to.deep.equal(
{
id: 2,
ownerId: 1,
mailId: 1,
mail: {
id: 1,
recipients: [{
id: 1,
MailRecipients: {
mailId: 1,
recipientId: 1
}
}]
}
}
);
});
}); });
}); });
...@@ -643,7 +745,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -643,7 +745,6 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
}); });
}); });
}); });
} }
// NOTE: mssql does not support changing an autoincrement primary key // NOTE: mssql does not support changing an autoincrement primary key
...@@ -677,17 +778,15 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -677,17 +778,15 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
}); });
}); });
} }
}); });
describe('Association column', () => { describe('association column', () => {
it('has correct type and name for non-id primary keys with non-integer type', function() { it('has correct type and name for non-id primary keys with non-integer type', function() {
const User = this.sequelize.define('UserPKBT', { const User = this.sequelize.define('UserPKBT', {
username: { username: {
type: DataTypes.STRING type: DataTypes.STRING
} }
}), });
self = this;
const Group = this.sequelize.define('GroupPKBT', { const Group = this.sequelize.define('GroupPKBT', {
name: { name: {
...@@ -698,14 +797,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -698,14 +797,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
User.belongsTo(Group); User.belongsTo(Group);
return self.sequelize.sync({ force: true }).then(() => { return this.sequelize.sync({ force: true }).then(() => {
expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING); expect(User.rawAttributes.GroupPKBTName.type).to.an.instanceof(DataTypes.STRING);
}); });
}); });
it('should support a non-primary key as the association column on a target without a primary key', function() { it('should support a non-primary key as the association column on a target without a primary key', function() {
const User = this.sequelize.define('User', { username: DataTypes.STRING }), const User = this.sequelize.define('User', { username: DataTypes.STRING });
Task = this.sequelize.define('Task', { title: DataTypes.STRING }); const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id'); User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'}); Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
...@@ -782,8 +881,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -782,8 +881,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
}); });
}); });
describe('Association options', () => { describe('association options', () => {
it('can specify data type for autogenerated relational keys', function() { it('can specify data type for auto-generated relational keys', function() {
const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }), const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }),
dataTypes = [DataTypes.INTEGER, DataTypes.BIGINT, DataTypes.STRING], dataTypes = [DataTypes.INTEGER, DataTypes.BIGINT, DataTypes.STRING],
self = this, self = this,
...@@ -878,97 +977,4 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => { ...@@ -878,97 +977,4 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
.throw ('Naming collision between attribute \'person\' and association \'person\' on model car. To remedy this, change either foreignKey or as in your association definition'); .throw ('Naming collision between attribute \'person\' and association \'person\' on model car. To remedy this, change either foreignKey or as in your association definition');
}); });
}); });
}); });
\ No newline at end of file
describe('Association', () => {
it('should set foreignKey on foreign table', function() {
const Mail = this.sequelize.define('mail', {}, { timestamps: false });
const Entry = this.sequelize.define('entry', {}, { timestamps: false });
const User = this.sequelize.define('user', {}, { timestamps: false });
Entry.belongsTo(User, { as: 'owner', foreignKey: { name: 'ownerId', allowNull: false } });
Entry.belongsTo(Mail, {
as: 'mail',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
Mail.belongsToMany(User, {
as: 'recipients',
through: 'MailRecipients',
otherKey: {
name: 'recipientId',
allowNull: false
},
foreignKey: {
name: 'mailId',
allowNull: false
},
timestamps: false
});
Mail.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'mailId',
allowNull: false
}
});
User.hasMany(Entry, {
as: 'entries',
foreignKey: {
name: 'ownerId',
allowNull: false
}
});
return this.sequelize.sync({ force: true })
.then(() => User.create({}))
.then(() => Mail.create({}))
.then(mail =>
Entry.create({ mailId: mail.id, ownerId: 1 })
.then(() => Entry.create({ mailId: mail.id, ownerId: 1 }))
// set recipients
.then(() => mail.setRecipients([1]))
)
.then(() => Entry.findAndCount({
offset: 0,
limit: 10,
order: [['id', 'DESC']],
include: [
{
association: Entry.associations.mail,
include: [
{
association: Mail.associations.recipients,
through: {
where: {
recipientId: 1
}
},
required: true
}
],
required: true
}
]
})).then(result => {
expect(result.count).to.equal(2);
expect(result.rows[0].get({ plain: true })).to.deep.equal(
{
id: 2,
ownerId: 1,
mailId: 1,
mail: {
id: 1,
recipients: [{
id: 1,
MailRecipients: {
mailId: 1,
recipientId: 1
}
}]
}
}
);
});
});
});
...@@ -443,7 +443,6 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { ...@@ -443,7 +443,6 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
}); });
describe('(1:N)', () => { describe('(1:N)', () => {
describe('hasSingle', () => { describe('hasSingle', () => {
beforeEach(function() { beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING }); this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING });
...@@ -1121,7 +1120,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { ...@@ -1121,7 +1120,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
}); });
}); });
describe('Foreign key constraints', () => { describe('foreign key constraints', () => {
describe('1:m', () => { describe('1:m', () => {
it('sets null by default', function() { it('sets null by default', function() {
const Task = this.sequelize.define('Task', { title: DataTypes.STRING }), const Task = this.sequelize.define('Task', { title: DataTypes.STRING }),
...@@ -1299,14 +1298,32 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { ...@@ -1299,14 +1298,32 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
expect(tasks).to.have.length(1); expect(tasks).to.have.length(1);
}); });
}); });
} }
}); });
}); });
describe('Association options', () => { describe('Association options', () => {
it('can specify data type for autogenerated relational keys', function() { it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true });
const Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
User.hasMany(Account);
expect(Account.rawAttributes.UserId).to.exist;
expect(Account.rawAttributes.UserId.field).to.equal('user_id');
});
it('should use model name when using camelcase', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: false });
const Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: false });
User.hasMany(Account);
expect(Account.rawAttributes.UserId).to.exist;
expect(Account.rawAttributes.UserId.field).to.equal('UserId');
});
it('can specify data type for auto-generated relational keys', function() {
const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }), const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }),
dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING], dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING],
self = this, self = this,
...@@ -1372,7 +1389,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { ...@@ -1372,7 +1389,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
} }
}); });
User.hasMany(Project, { foreignKey: Project.rawAttributes.user_id}); User.hasMany(Project, { foreignKey: Project.rawAttributes.user_id });
expect(Project.rawAttributes.user_id).to.be.ok; expect(Project.rawAttributes.user_id).to.be.ok;
expect(Project.rawAttributes.user_id.references.model).to.equal(User.getTableName()); expect(Project.rawAttributes.user_id.references.model).to.equal(User.getTableName());
...@@ -1504,7 +1521,10 @@ describe(Support.getTestDialectTeaser('HasMany'), () => { ...@@ -1504,7 +1521,10 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
this.Task = this.sequelize.define('Task', this.Task = this.sequelize.define('Task',
{ title: Sequelize.STRING, userEmail: Sequelize.STRING, taskStatus: Sequelize.STRING }); { title: Sequelize.STRING, userEmail: Sequelize.STRING, taskStatus: Sequelize.STRING });
this.User.hasMany(this.Task, {foreignKey: 'userEmail', sourceKey: 'mail'}); this.User.hasMany(this.Task, {
foreignKey: 'userEmail',
sourceKey: 'email'
});
return this.sequelize.sync({ force: true }); return this.sequelize.sync({ force: true });
}); });
......
...@@ -10,7 +10,7 @@ const chai = require('chai'), ...@@ -10,7 +10,7 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('HasOne'), () => { describe(Support.getTestDialectTeaser('HasOne'), () => {
describe('Model.associations', () => { describe('Model.associations', () => {
it('should store all assocations when associting to the same table multiple times', function() { it('should store all associations when associating to the same table multiple times', function() {
const User = this.sequelize.define('User', {}), const User = this.sequelize.define('User', {}),
Group = this.sequelize.define('Group', {}); Group = this.sequelize.define('Group', {});
...@@ -18,7 +18,9 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { ...@@ -18,7 +18,9 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' }); Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
Group.hasOne(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' }); Group.hasOne(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' });
expect(Object.keys(Group.associations)).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']); expect(
Object.keys(Group.associations)
).to.deep.equal(['User', 'primaryUsers', 'secondaryUsers']);
}); });
}); });
...@@ -59,8 +61,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { ...@@ -59,8 +61,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
}); });
}); });
describe('getAssociation', () => {
describe('getAssocation', () => {
if (current.dialect.supports.transactions) { if (current.dialect.supports.transactions) {
it('supports transactions', function() { it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => { return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
...@@ -386,13 +387,14 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { ...@@ -386,13 +387,14 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
}); });
describe('foreign key', () => { describe('foreign key', () => {
it('should lowercase foreign keys when using underscored', function() { it('should setup underscored field with foreign keys when using underscored', function() {
const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }), const User = this.sequelize.define('User', { username: Sequelize.STRING }, { underscored: true }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true }); Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
Account.hasOne(User); Account.hasOne(User);
expect(User.rawAttributes.account_id).to.exist; expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('account_id');
}); });
it('should use model name when using camelcase', function() { it('should use model name when using camelcase', function() {
...@@ -402,6 +404,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => { ...@@ -402,6 +404,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Account.hasOne(User); Account.hasOne(User);
expect(User.rawAttributes.AccountId).to.exist; expect(User.rawAttributes.AccountId).to.exist;
expect(User.rawAttributes.AccountId.field).to.equal('AccountId');
}); });
it('should support specifying the field of a foreign key', function() { it('should support specifying the field of a foreign key', function() {
......
...@@ -15,6 +15,14 @@ const Promise = current.Promise; ...@@ -15,6 +15,14 @@ const Promise = current.Promise;
const AssociationError = require(__dirname + '/../../../lib/errors').AssociationError; const AssociationError = require(__dirname + '/../../../lib/errors').AssociationError;
describe(Support.getTestDialectTeaser('belongsToMany'), () => { describe(Support.getTestDialectTeaser('belongsToMany'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.belongsToMany();
}).to.throw('User.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
});
it('should not inherit scopes from parent to join table', () => { it('should not inherit scopes from parent to join table', () => {
const A = current.define('a'), const A = current.define('a'),
B = current.define('b', {}, { B = current.define('b', {}, {
......
...@@ -7,6 +7,14 @@ const chai = require('chai'), ...@@ -7,6 +7,14 @@ const chai = require('chai'),
current = Support.sequelize; current = Support.sequelize;
describe(Support.getTestDialectTeaser('belongsTo'), () => { describe(Support.getTestDialectTeaser('belongsTo'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.belongsTo();
}).to.throw('User.belongsTo called with something that\'s not a subclass of Sequelize.Model');
});
it('should not override custom methods with association mixin', () => { it('should not override custom methods with association mixin', () => {
const methods = { const methods = {
getTask: 'get', getTask: 'get',
......
...@@ -13,6 +13,14 @@ const chai = require('chai'), ...@@ -13,6 +13,14 @@ const chai = require('chai'),
Promise = current.Promise; Promise = current.Promise;
describe(Support.getTestDialectTeaser('hasMany'), () => { describe(Support.getTestDialectTeaser('hasMany'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.hasMany();
}).to.throw('User.hasMany called with something that\'s not a subclass of Sequelize.Model');
});
describe('optimizations using bulk create, destroy and update', () => { describe('optimizations using bulk create, destroy and update', () => {
const User =current.define('User', { username: DataTypes.STRING }), const User =current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING }); Task = current.define('Task', { title: DataTypes.STRING });
......
...@@ -8,6 +8,14 @@ const chai = require('chai'), ...@@ -8,6 +8,14 @@ const chai = require('chai'),
current = Support.sequelize; current = Support.sequelize;
describe(Support.getTestDialectTeaser('hasOne'), () => { describe(Support.getTestDialectTeaser('hasOne'), () => {
it('throws when invalid model is passed', () => {
const User = current.define('User');
expect(() => {
User.hasOne();
}).to.throw('User.hasOne called with something that\'s not a subclass of Sequelize.Model');
});
it('properly use the `as` key to generate foreign key name', () => { it('properly use the `as` key to generate foreign key name', () => {
const User = current.define('User', { username: DataTypes.STRING }), const User = current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING }); Task = current.define('Task', { title: DataTypes.STRING });
......
'use strict';
const chai = require('chai'),
expect = chai.expect,
Support = require(__dirname + '/../support'),
DataTypes = require(__dirname + '/../../../lib/data-types'),
Sequelize = require('../../../index');
describe(Support.getTestDialectTeaser('Model'), () => {
describe('options.underscored', () => {
beforeEach(function() {
this.N = this.sequelize.define('N', {
id: {
type: DataTypes.CHAR(10),
primaryKey: true,
field: 'n_id'
}
}, {
underscored: true
});
this.M = this.sequelize.define('M', {
id: {
type: Sequelize.CHAR(20),
primaryKey: true,
field: 'm_id'
}
}, {
underscored: true
});
this.NM = this.sequelize.define('NM', {});
});
it('should properly set field when defining', function() {
expect(this.N.rawAttributes['id'].field).to.equal('n_id');
expect(this.M.rawAttributes['id'].field).to.equal('m_id');
});
it('hasOne does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.M.hasOne(this.N, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('belongsTo does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.N.belongsTo(this.M, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('hasOne/belongsTo does not override already defined field', function() {
this.N.rawAttributes['mId'] = {
type: Sequelize.CHAR(20),
field: 'n_m_id'
};
this.N.refreshAttributes();
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
this.N.belongsTo(this.M, { foreignKey: 'mId' });
this.M.hasOne(this.N, { foreignKey: 'mId' });
expect(this.N.rawAttributes['mId'].field).to.equal('n_m_id');
});
it('hasMany does not override already defined field', function() {
this.M.rawAttributes['nId'] = {
type: Sequelize.CHAR(20),
field: 'nana_id'
};
this.M.refreshAttributes();
expect(this.M.rawAttributes['nId'].field).to.equal('nana_id');
this.N.hasMany(this.M, { foreignKey: 'nId' });
this.M.belongsTo(this.N, { foreignKey: 'nId' });
expect(this.M.rawAttributes['nId'].field).to.equal('nana_id');
});
it('belongsToMany does not override already defined field', function() {
this.NM = this.sequelize.define('NM', {
n_id: {
type: Sequelize.CHAR(10),
field: 'nana_id'
},
m_id: {
type: Sequelize.CHAR(20),
field: 'mama_id'
}
}, {
underscored: true
});
this.N.belongsToMany(this.M, { through: this.NM, foreignKey: 'n_id' });
this.M.belongsToMany(this.N, { through: this.NM, foreignKey: 'm_id' });
expect(this.NM.rawAttributes['n_id'].field).to.equal('nana_id');
expect(this.NM.rawAttributes['m_id'].field).to.equal('mama_id');
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!