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

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 */ }, {
// timestamps are enabled
paranoid: true,
// don't use camelcase for automatically added attributes but underscore style
// so updatedAt will be updated_at
// Will automatically set field option for all attributes to snake cased name.
// Does not override attribute with field option already defined
underscored: true,
// disable the modification of table names; By default, sequelize will automatically
......
......@@ -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.
*/
class Association {
constructor(source, target, options) {
options = options || {};
constructor(source, target, options = {}) {
/**
* @type {Model}
*/
this.source = source;
/**
* @type {Model}
*/
this.target = target;
this.options = options;
this.scope = options.scope;
this.isSelfAssociation = this.source === this.target;
this.as = options.as;
/**
* The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany`
* @type {string}
......@@ -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)) {
objs = [objs];
/**
* Normalize input
*
* @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 => {
if (element instanceof this.target) return element;
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = obj;
return this.target.build(tmpInstance, {
isNewRecord: false
tmpInstance[this.target.primaryKeyAttribute] = element;
return this.target.build(tmpInstance, { isNewRecord: false });
});
}
return obj;
}, this);
}
inspect() {
return this.as;
}
......
......@@ -121,12 +121,11 @@ class BelongsToMany extends Association {
}
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
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -139,17 +138,11 @@ class BelongsToMany extends Association {
}
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.target.options.underscored
),
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
].join('_')
);
}
......@@ -188,13 +181,13 @@ class BelongsToMany extends Association {
this.otherKey = this.paired.foreignKey;
}
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) {
delete this.through.model.rawAttributes[this.paired.otherKey];
}
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
this.paired._injectAttributes();
}
}
}
......@@ -226,8 +219,7 @@ class BelongsToMany extends Association {
// the id is in the target table
// or in an extra table which connects two tables
injectAttributes() {
_injectAttributes() {
this.identifier = this.foreignKey;
this.foreignIdentifier = this.otherKey;
......@@ -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.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.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
......@@ -309,8 +303,6 @@ class BelongsToMany extends Association {
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, {
foreignKey: this.foreignKey
});
......
......@@ -41,17 +41,15 @@ class BelongsTo extends Association {
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
this.foreignKey = Utils.camelize(
[
Utils.underscoredIf(this.as, this.source.options.underscored),
this.as,
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
].join('_')
);
}
this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
......@@ -75,7 +73,7 @@ class BelongsTo extends Association {
}
// the id is in the source table
injectAttributes() {
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
......@@ -92,10 +90,10 @@ class BelongsTo extends Association {
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
this.source.refreshAttributes();
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
......
......@@ -64,12 +64,11 @@ class HasMany extends Association {
}
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
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -78,25 +77,25 @@ class HasMany extends Association {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
/*
* Source key setup
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
if (this.target.rawAttributes[this.sourceKey]) {
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyField = this.sourceKey;
}
this.sourceIdentifier = this.sourceKey;
if (this.source.fieldRawAttributesMap[this.sourceKey]) {
this.sourceKeyAttribute = this.source.fieldRawAttributesMap[this.sourceKey].fieldName;
if (this.source.rawAttributes[this.sourceKey]) {
this.sourceKeyAttribute = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
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
const plural = Utils.uppercaseFirst(this.options.name.plural);
const singular = Utils.uppercaseFirst(this.options.name.singular);
this.associationAccessor = this.as;
this.accessors = {
get: 'get' + plural,
set: 'set' + plural,
......@@ -113,9 +112,11 @@ class HasMany extends Association {
// the id is in the target table
// or in an extra table which connects two tables
injectAttributes() {
_injectAttributes() {
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, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].type,
allowNull: true
......@@ -126,15 +127,17 @@ class HasMany extends Association {
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions, this.sourceKeyField);
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.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);
return this;
......
......@@ -40,12 +40,11 @@ class HasOne extends Association {
}
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
].join('_'),
!this.source.options.underscored
].join('_')
);
}
......@@ -71,7 +70,7 @@ class HasOne extends Association {
}
// the id is in the target table
injectAttributes() {
_injectAttributes() {
const newAttributes = {};
const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
......@@ -79,9 +78,6 @@ class HasOne extends Association {
type: this.options.keyType || keyType,
allowNull: true
});
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
......@@ -89,11 +85,13 @@ class HasOne extends Association {
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.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
......
'use strict';
const _ = require('lodash');
function checkNamingCollision(association) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) {
throw new Error(
'Naming collision between attribute \'' + association.as +
'\' and association \'' + association.as + '\' on model ' + association.source.name +
`Naming collision between attribute '${association.as}'` +
` and association '${association.as}' on model ${association.source.name}` +
'. To remedy this, change either foreignKey or as in your association definition'
);
}
......@@ -15,14 +13,12 @@ exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints(newAttribute, source, target, options, key) {
// 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) {
// Find primary keys: composite keys not supported with this approach
const primaryKeys = _.chain(source.rawAttributes).keys()
.filter(key => source.rawAttributes[key].primaryKey)
.map(key => source.rawAttributes[key].field || key).value();
const primaryKeys = Object.keys(source.primaryKeys)
.map(primaryKeyAttribute => source.rawAttributes[primaryKeyAttribute].field || primaryKeyAttribute);
if (primaryKeys.length === 1) {
if (source._schema) {
......
......@@ -7,16 +7,21 @@ const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many');
const BelongsTo = require('./belongs-to');
function isModel(model, sequelize) {
return model
&& model.prototype
&& model.prototype instanceof sequelize.Model;
}
const Mixin = {
hasMany(target, options) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not a subclass of Sequelize.Model');
hasMany(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.hasMany called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// 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.useHooks = options.hooks;
......@@ -26,32 +31,31 @@ const Mixin = {
const association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association._injectAttributes();
association.mixin(source.prototype);
return association;
},
belongsToMany(targetModel, options) { // testhint options:none
if (!targetModel || !targetModel.prototype || !(targetModel.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
belongsToMany(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
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)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
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
const association = new BelongsToMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association;
const association = new BelongsToMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association.mixin(sourceModel.prototype);
association._injectAttributes();
association.mixin(source.prototype);
return association;
},
......@@ -76,15 +80,14 @@ const Mixin = {
// The logic for hasOne and belongsTo is exactly the same
function singleLinked(Type) {
return function(target, options) { // testhint options:none
if (!target || !target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not a subclass of Sequelize.Model');
return function(target, options = {}) { // testhint options:none
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.${Utils.lowercaseFirst(Type.name)} called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// 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.useHooks = options.hooks;
......@@ -92,7 +95,7 @@ function singleLinked(Type) {
const association = new Type(source, target, _.extend(options, source.options));
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association._injectAttributes();
association.mixin(source.prototype);
return association;
......@@ -100,7 +103,6 @@ function singleLinked(Type) {
}
Mixin.hasOne = singleLinked(HasOne);
Mixin.belongsTo = singleLinked(BelongsTo);
module.exports = Mixin;
......
......@@ -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.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.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.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.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.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 {String} [options.name.singular=Utils.singularize(modelName)]
......@@ -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[].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 {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.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.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.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 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 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.schema='public']
* @param {String} [options.engine]
......@@ -747,7 +746,6 @@ class Model {
validate: {},
freezeTableName: false,
underscored: false,
underscoredAll: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: null,
......@@ -766,10 +764,10 @@ class Model {
this.associations = {};
this._setupHooks(options.hooks);
this.underscored = this.underscored || this.underscoredAll;
this.underscored = this.options.underscored;
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 {
this.tableName = this.options.tableName;
}
......@@ -789,7 +787,6 @@ class Model {
});
this.rawAttributes = _.mapValues(attributes, (attribute, name) => {
attribute = this.sequelize.normalizeAttribute(attribute);
if (attribute.type === undefined) {
......@@ -804,20 +801,22 @@ class Model {
});
this.primaryKeys = {};
this._timestampAttributes = {};
// Setup names of timestamp attributes
this._timestampAttributes = {};
if (this.options.timestamps) {
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) {
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) {
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) {
this._versionAttribute = typeof this.options.version === 'string' ? this.options.version : 'version';
}
......@@ -947,7 +946,7 @@ class Model {
definition._modelAttribute = true;
if (definition.field === undefined) {
definition.field = name;
definition.field = Utils.underscoredIf(name, this.underscored);
}
if (definition.primaryKey === true) {
......
......@@ -15,8 +15,6 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
describe('getAssociations', () => {
beforeEach(function() {
const self = this;
this.User = this.sequelize.define('User', { username: DataTypes.STRING });
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN });
......@@ -25,13 +23,13 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
return this.sequelize.sync({ force: true }).then(() => {
return Promise.all([
self.User.create({ username: 'John'}),
self.Task.create({ title: 'Get rich', active: true}),
self.Task.create({ title: 'Die trying', active: false})
this.User.create({ username: 'John'}),
this.Task.create({ title: 'Get rich', active: true}),
this.Task.create({ title: 'Die trying', active: false})
]);
}).spread((john, task1, task2) => {
self.tasks = [task1, task2];
self.user = john;
this.tasks = [task1, task2];
this.user = john;
return john.setTasks([task1, task2]);
});
});
......@@ -1255,8 +1253,8 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
const attributes = this.sequelize.model('user_places').rawAttributes;
expect(attributes.place_id).to.be.ok;
expect(attributes.user_id).to.be.ok;
expect(attributes.PlaceId.field).to.equal('place_id');
expect(attributes.UserId.field).to.equal('user_id');
});
it('should infer otherKey from paired BTM relationship with a through string defined', function() {
......@@ -2276,10 +2274,12 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
PersonChildren = this.sequelize.define('PersonChildren', {}, {underscored: true});
Children = Person.belongsToMany(Person, { as: 'Children', through: PersonChildren});
expect(Children.foreignKey).to.equal('person_id');
expect(Children.otherKey).to.equal('child_id');
expect(Children.foreignKey).to.equal('PersonId');
expect(Children.otherKey).to.equal('ChildId');
expect(PersonChildren.rawAttributes[Children.foreignKey]).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'),
describe(Support.getTestDialectTeaser('BelongsTo'), () => {
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', {}),
Group = this.sequelize.define('Group', {});
......@@ -20,7 +20,9 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
Group.belongsTo(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
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'), () => {
});
describe('getAssociation', () => {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
......@@ -452,13 +453,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
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 }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
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() {
......@@ -468,6 +470,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
User.belongsTo(Account);
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() {
......@@ -497,14 +500,113 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
return user.getAccount();
});
}).then(user => {
// the sql query should correctly look at task_id instead of taskId
expect(user).to.not.be.null;
return User.findOne({
where: {username: 'foo'},
include: [Account]
});
}).then(task => {
expect(task.Account).to.exist;
}).then(user => {
// 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'), () => {
});
});
});
}
// NOTE: mssql does not support changing an autoincrement primary key
......@@ -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() {
const User = this.sequelize.define('UserPKBT', {
username: {
type: DataTypes.STRING
}
}),
self = this;
});
const Group = this.sequelize.define('GroupPKBT', {
name: {
......@@ -698,14 +797,14 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
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);
});
});
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 }),
Task = this.sequelize.define('Task', { title: DataTypes.STRING });
const User = this.sequelize.define('User', { username: DataTypes.STRING });
const Task = this.sequelize.define('Task', { title: DataTypes.STRING });
User.removeAttribute('id');
Task.belongsTo(User, { foreignKey: 'user_name', targetKey: 'username'});
......@@ -782,8 +881,8 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
});
describe('Association options', () => {
it('can specify data type for autogenerated relational keys', function() {
describe('association options', () => {
it('can specify data type for auto-generated relational keys', function() {
const User = this.sequelize.define('UserXYZ', { username: DataTypes.STRING }),
dataTypes = [DataTypes.INTEGER, DataTypes.BIGINT, DataTypes.STRING],
self = this,
......@@ -879,96 +978,3 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
});
});
});
\ 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'), () => {
});
describe('(1:N)', () => {
describe('hasSingle', () => {
beforeEach(function() {
this.Article = this.sequelize.define('Article', { 'title': DataTypes.STRING });
......@@ -1121,7 +1120,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
});
});
describe('Foreign key constraints', () => {
describe('foreign key constraints', () => {
describe('1:m', () => {
it('sets null by default', function() {
const Task = this.sequelize.define('Task', { title: DataTypes.STRING }),
......@@ -1299,14 +1298,32 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
expect(tasks).to.have.length(1);
});
});
}
});
});
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 }),
dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING],
self = this,
......@@ -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.references.model).to.equal(User.getTableName());
......@@ -1504,7 +1521,10 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
this.Task = this.sequelize.define('Task',
{ 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 });
});
......
......@@ -10,7 +10,7 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('HasOne'), () => {
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', {}),
Group = this.sequelize.define('Group', {});
......@@ -18,7 +18,9 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' });
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'), () => {
});
});
describe('getAssocation', () => {
describe('getAssociation', () => {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).then(sequelize => {
......@@ -386,13 +387,14 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
});
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 }),
Account = this.sequelize.define('Account', { name: Sequelize.STRING }, { underscored: true });
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() {
......@@ -402,6 +404,7 @@ describe(Support.getTestDialectTeaser('HasOne'), () => {
Account.hasOne(User);
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() {
......
......@@ -15,6 +15,14 @@ const Promise = current.Promise;
const AssociationError = require(__dirname + '/../../../lib/errors').AssociationError;
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', () => {
const A = current.define('a'),
B = current.define('b', {}, {
......
......@@ -7,6 +7,14 @@ const chai = require('chai'),
current = Support.sequelize;
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', () => {
const methods = {
getTask: 'get',
......
......@@ -13,6 +13,14 @@ const chai = require('chai'),
Promise = current.Promise;
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', () => {
const User =current.define('User', { username: DataTypes.STRING }),
Task = current.define('Task', { title: DataTypes.STRING });
......
......@@ -8,6 +8,14 @@ const chai = require('chai'),
current = Support.sequelize;
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', () => {
const User = current.define('User', { username: 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!