diff --git a/lib/model.js b/lib/model.js index 5ac6f28fc248..2f03a794e28c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2530,163 +2530,313 @@ class Model { return Promise.resolve([]); } - options = Object.assign({ - validate: false, - hooks: true, - individualHooks: false, - ignoreDuplicates: false - }, options); - - options.fields = options.fields || Object.keys(this.rawAttributes); - const dialect = this.sequelize.options.dialect; + const now = Utils.now(this.sequelize.options.dialect); - if (options.ignoreDuplicates && ['mssql'].includes(dialect)) { - return Promise.reject(new Error(`${dialect} does not support the ignoreDuplicates option.`)); - } - if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'postgres')) { - return Promise.reject(new Error(`${dialect} does not support the updateOnDuplicate option.`)); - } + options.model = this; - if (options.updateOnDuplicate !== undefined) { - if (Array.isArray(options.updateOnDuplicate) && options.updateOnDuplicate.length) { - options.updateOnDuplicate = _.intersection( - _.without(Object.keys(this.tableAttributes), this._timestampAttributes.createdAt), - options.updateOnDuplicate - ); - } else { - return Promise.reject(new Error('updateOnDuplicate option only supports non-empty array.')); + if (!options.includeValidated) { + this._conformIncludes(options, this); + if (options.include) { + this._expandIncludeAll(options); + this._validateIncludedElements(options); } } - options.model = this; + const instances = records.map(values => this.build(values, { isNewRecord: true, include: options.include })); - const createdAtAttr = this._timestampAttributes.createdAt; - const updatedAtAttr = this._timestampAttributes.updatedAt; - const now = Utils.now(this.sequelize.options.dialect); + const recursiveBulkCreate = (instances, options) => { + options = Object.assign({ + validate: false, + hooks: true, + individualHooks: false, + ignoreDuplicates: false + }, options); - let instances = records.map(values => this.build(values, { isNewRecord: true })); + if (options.returning === undefined) { + if (options.association) { + options.returning = false; + } else { + options.returning = true; + } + } - return Promise.try(() => { - // Run before hook - if (options.hooks) { - return this.runHooks('beforeBulkCreate', instances, options); + if (options.ignoreDuplicates && ['mssql'].includes(dialect)) { + return Promise.reject(new Error(`${dialect} does not support the ignoreDuplicates option.`)); } - }).then(() => { - // Validate - if (options.validate) { - const errors = new Promise.AggregateError(); - const validateOptions = _.clone(options); - validateOptions.hooks = options.individualHooks; - - return Promise.map(instances, instance => - instance.validate(validateOptions).catch(err => { - errors.push(new sequelizeErrors.BulkRecordError(err, instance)); - }) - ).then(() => { - delete options.skip; - if (errors.length) { - throw errors; - } - }); + if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'postgres')) { + return Promise.reject(new Error(`${dialect} does not support the updateOnDuplicate option.`)); } - }).then(() => { - if (options.individualHooks) { - // Create each instance individually - return Promise.map(instances, instance => { - const individualOptions = _.clone(options); - delete individualOptions.fields; - delete individualOptions.individualHooks; - delete individualOptions.ignoreDuplicates; - individualOptions.validate = false; - individualOptions.hooks = true; - - return instance.save(individualOptions); - }).then(_instances => { - instances = _instances; - }); + + const model = options.model; + + options.fields = options.fields || Object.keys(model.rawAttributes); + const createdAtAttr = model._timestampAttributes.createdAt; + const updatedAtAttr = model._timestampAttributes.updatedAt; + + if (options.updateOnDuplicate !== undefined) { + if (Array.isArray(options.updateOnDuplicate) && options.updateOnDuplicate.length) { + options.updateOnDuplicate = _.intersection( + _.without(Object.keys(model.tableAttributes), createdAtAttr), + options.updateOnDuplicate + ); + } else { + return Promise.reject(new Error('updateOnDuplicate option only supports non-empty array.')); + } } - // Create all in one query - // Recreate records from instances to represent any changes made in hooks or validation - records = instances.map(instance => { - const values = instance.dataValues; - // set createdAt/updatedAt attributes - if (createdAtAttr && !values[createdAtAttr]) { - values[createdAtAttr] = now; - if (!options.fields.includes(createdAtAttr)) { - options.fields.push(createdAtAttr); - } + return Promise.try(() => { + // Run before hook + if (options.hooks) { + return model.runHooks('beforeBulkCreate', instances, options); } - if (updatedAtAttr && !values[updatedAtAttr]) { - values[updatedAtAttr] = now; - if (!options.fields.includes(updatedAtAttr)) { - options.fields.push(updatedAtAttr); - } + }).then(() => { + // Validate + if (options.validate) { + const errors = new Promise.AggregateError(); + const validateOptions = _.clone(options); + validateOptions.hooks = options.individualHooks; + + return Promise.map(instances, instance => + instance.validate(validateOptions).catch(err => { + errors.push(new sequelizeErrors.BulkRecordError(err, instance)); + }) + ).then(() => { + delete options.skip; + if (errors.length) { + throw errors; + } + }); + } + }).then(() => { + if (options.individualHooks) { + // Create each instance individually + return Promise.map(instances, instance => { + const individualOptions = _.clone(options); + delete individualOptions.fields; + delete individualOptions.individualHooks; + delete individualOptions.ignoreDuplicates; + individualOptions.validate = false; + individualOptions.hooks = true; + + return instance.save(individualOptions); + }); } - instance.dataValues = Utils.mapValueFieldNames(values, options.fields, this); + return Promise.resolve().then(() => { + if (!options.include || !options.include.length) return; - const out = Object.assign({}, instance.dataValues); - for (const key of this._virtualAttributes) { - delete out[key]; - } - return out; - }); + // Nested creation for BelongsTo relations + return Promise.map(options.include.filter(include => include.association instanceof BelongsTo), include => { + const associationInstances = []; + const associationInstanceIndexToInstanceMap = []; - // Map attributes to fields for serial identification - const fieldMappedAttributes = {}; - for (const attr in this.tableAttributes) { - fieldMappedAttributes[this.rawAttributes[attr].field || attr] = this.rawAttributes[attr]; - } + for (const instance of instances) { + const associationInstance = instance.get(include.as); + if (associationInstance) { + associationInstances.push(associationInstance); + associationInstanceIndexToInstanceMap.push(instance); + } + } - // Map updateOnDuplicate attributes to fields - if (options.updateOnDuplicate) { - options.updateOnDuplicate = options.updateOnDuplicate.map(attr => this.rawAttributes[attr].field || attr); - // Get primary keys for postgres to enable updateOnDuplicate - options.upsertKeys = _.chain(this.primaryKeys).values().map('fieldName').value(); - if (Object.keys(this.uniqueKeys).length > 0) { - options.upsertKeys = _.chain(this.uniqueKeys).values().filter(c => c.fields.length === 1).map('column').value(); - } - } + if (!associationInstances.length) { + return; + } - // Map returning attributes to fields - if (options.returning && Array.isArray(options.returning)) { - options.returning = options.returning.map(attr => this.rawAttributes[attr].field || attr); - } + const includeOptions = _(Utils.cloneDeep(include)) + .omit(['association']) + .defaults({ + transaction: options.transaction, + logging: options.logging + }).value(); - return this.QueryInterface.bulkInsert(this.getTableName(options), records, options, fieldMappedAttributes).then(results => { - if (Array.isArray(results)) { - results.forEach((result, i) => { - if (instances[i] && !instances[i].get(this.primaryKeyAttribute)) { - instances[i].dataValues[this.primaryKeyField] = result[this.primaryKeyField]; + return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => { + for (const idx in associationInstances) { + const associationInstance = associationInstances[idx]; + const instance = associationInstanceIndexToInstanceMap[idx]; + + instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging }); + } + }); + }); + }).then(() => { + // Create all in one query + // Recreate records from instances to represent any changes made in hooks or validation + records = instances.map(instance => { + const values = instance.dataValues; + + // set createdAt/updatedAt attributes + if (createdAtAttr && !values[createdAtAttr]) { + values[createdAtAttr] = now; + if (!options.fields.includes(createdAtAttr)) { + options.fields.push(createdAtAttr); + } + } + if (updatedAtAttr && !values[updatedAtAttr]) { + values[updatedAtAttr] = now; + if (!options.fields.includes(updatedAtAttr)) { + options.fields.push(updatedAtAttr); + } } + + const out = Object.assign({}, Utils.mapValueFieldNames(values, options.fields, model)); + for (const key of model._virtualAttributes) { + delete out[key]; + } + return out; }); - } - return results; - }); - }).then(() => { - // map fields back to attributes - instances.forEach(instance => { - for (const attr in this.rawAttributes) { - if (this.rawAttributes[attr].field && - instance.dataValues[this.rawAttributes[attr].field] !== undefined && - this.rawAttributes[attr].field !== attr - ) { - instance.dataValues[attr] = instance.dataValues[this.rawAttributes[attr].field]; - delete instance.dataValues[this.rawAttributes[attr].field]; + + // Map attributes to fields for serial identification + const fieldMappedAttributes = {}; + for (const attr in model.tableAttributes) { + fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr]; + } + + // Map updateOnDuplicate attributes to fields + if (options.updateOnDuplicate) { + options.updateOnDuplicate = options.updateOnDuplicate.map(attr => model.rawAttributes[attr].field || attr); + // Get primary keys for postgres to enable updateOnDuplicate + options.upsertKeys = _.chain(model.primaryKeys).values().map('fieldName').value(); + if (Object.keys(model.uniqueKeys).length > 0) { + options.upsertKeys = _.chain(model.uniqueKeys).values().filter(c => c.fields.length === 1).map('column').value(); + } + } + + // Map returning attributes to fields + if (options.returning && Array.isArray(options.returning)) { + options.returning = options.returning.map(attr => model.rawAttributes[attr].field || attr); } - instance._previousDataValues[attr] = instance.dataValues[attr]; - instance.changed(attr, false); + + return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => { + if (Array.isArray(results)) { + results.forEach((result, i) => { + const instance = instances[i]; + + for (const key in result) { + if (!instance || key === model.primaryKeyAttribute && + instance.get(model.primaryKeyAttribute) && + ['mysql', 'mariadb', 'sqlite'].includes(dialect)) { + // The query.js for these DBs is blind, it autoincrements the + // primarykey value, even if it was set manually. Also, it can + // return more results than instances, bug?. + continue; + } + if (Object.prototype.hasOwnProperty.call(result, key)) { + const record = result[key]; + + const attr = _.find(model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key); + + instance.dataValues[attr && attr.fieldName || key] = record; + } + } + }); + } + return results; + }); + }); + }).then(() => { + if (!options.include || !options.include.length) return; + + // Nested creation for HasOne/HasMany/BelongsToMany relations + return Promise.map(options.include.filter(include => !(include.association instanceof BelongsTo || + include.parent && include.parent.association instanceof BelongsToMany)), include => { + const associationInstances = []; + const associationInstanceIndexToInstanceMap = []; + + for (const instance of instances) { + let associated = instance.get(include.as); + if (!Array.isArray(associated)) associated = [associated]; + + for (const associationInstance of associated) { + if (associationInstance) { + if (!(include.association instanceof BelongsToMany)) { + associationInstance.set(include.association.foreignKey, instance.get(include.association.sourceKey || instance.constructor.primaryKeyAttribute, { raw: true }), { raw: true }); + Object.assign(associationInstance, include.association.scope); + } + associationInstances.push(associationInstance); + associationInstanceIndexToInstanceMap.push(instance); + } + } + } + + if (!associationInstances.length) { + return; + } + + const includeOptions = _(Utils.cloneDeep(include)) + .omit(['association']) + .defaults({ + transaction: options.transaction, + logging: options.logging + }).value(); + + return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => { + if (include.association instanceof BelongsToMany) { + const valueSets = []; + + for (const idx in associationInstances) { + const associationInstance = associationInstances[idx]; + const instance = associationInstanceIndexToInstanceMap[idx]; + + const values = {}; + values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true }); + values[include.association.otherKey] = associationInstance.get(associationInstance.constructor.primaryKeyAttribute, { raw: true }); + + // Include values defined in the association + Object.assign(values, include.association.through.scope); + if (associationInstance[include.association.through.model.name]) { + for (const attr of Object.keys(include.association.through.model.rawAttributes)) { + if (include.association.through.model.rawAttributes[attr]._autoGenerated || + attr === include.association.foreignKey || + attr === include.association.otherKey || + typeof associationInstance[include.association.through.model.name][attr] === undefined) { + continue; + } + values[attr] = associationInstance[include.association.through.model.name][attr]; + } + } + + valueSets.push(values); + } + + const throughOptions = _(Utils.cloneDeep(include)) + .omit(['association', 'attributes']) + .defaults({ + transaction: options.transaction, + logging: options.logging + }).value(); + throughOptions.model = include.association.throughModel; + const throughInstances = include.association.throughModel.bulkBuild(valueSets, throughOptions); + + return recursiveBulkCreate(throughInstances, throughOptions); + } + }); + }); + }).then(() => { + // map fields back to attributes + instances.forEach(instance => { + for (const attr in model.rawAttributes) { + if (model.rawAttributes[attr].field && + instance.dataValues[model.rawAttributes[attr].field] !== undefined && + model.rawAttributes[attr].field !== attr + ) { + instance.dataValues[attr] = instance.dataValues[model.rawAttributes[attr].field]; + delete instance.dataValues[model.rawAttributes[attr].field]; + } + instance._previousDataValues[attr] = instance.dataValues[attr]; + instance.changed(attr, false); + } + instance.isNewRecord = false; + }); + + // Run after hook + if (options.hooks) { + return model.runHooks('afterBulkCreate', instances, options); } - instance.isNewRecord = false; - }); + }).then(() => instances); + }; - // Run after hook - if (options.hooks) { - return this.runHooks('afterBulkCreate', instances, options); - } - }).then(() => instances); + return recursiveBulkCreate(instances, options); } /** diff --git a/test/integration/model/bulk-create.test.js b/test/integration/model/bulk-create.test.js index 0bee321e2419..5c9d76257a90 100644 --- a/test/integration/model/bulk-create.test.js +++ b/test/integration/model/bulk-create.test.js @@ -152,7 +152,7 @@ describe(Support.getTestDialectTeaser('Model'), () => { if (dialect === 'postgres') { expect(sql).to.include('INSERT INTO "Beers" ("id","style","createdAt","updatedAt") VALUES (DEFAULT'); } else if (dialect === 'mssql') { - expect(sql).to.include('INSERT INTO [Beers] ([style],[createdAt],[updatedAt]) VALUES'); + expect(sql).to.include('INSERT INTO [Beers] ([style],[createdAt],[updatedAt]) '); } else { // mysql, sqlite expect(sql).to.include('INSERT INTO `Beers` (`id`,`style`,`createdAt`,`updatedAt`) VALUES (NULL'); } diff --git a/test/integration/model/bulk-create/include.test.js b/test/integration/model/bulk-create/include.test.js new file mode 100644 index 000000000000..581f60f4d6c7 --- /dev/null +++ b/test/integration/model/bulk-create/include.test.js @@ -0,0 +1,632 @@ +'use strict'; + +const chai = require('chai'), + Sequelize = require('../../../../index'), + expect = chai.expect, + Support = require('../../support'), + DataTypes = require('../../../../lib/data-types'); + +describe(Support.getTestDialectTeaser('Model'), () => { + describe('bulkCreate', () => { + describe('include', () => { + it('should bulkCreate data for BelongsTo relations', function() { + const Product = this.sequelize.define('Product', { + title: Sequelize.STRING + }, { + hooks: { + afterBulkCreate(products) { + products.forEach(product => { + product.isIncludeCreatedOnAfterCreate = !!(product.User && product.User.id); + }); + } + } + }); + const User = this.sequelize.define('User', { + first_name: Sequelize.STRING, + last_name: Sequelize.STRING + }, { + hooks: { + beforeBulkCreate(users, options) { + users.forEach(user => { + user.createOptions = options; + }); + } + } + }); + + Product.belongsTo(User); + + return this.sequelize.sync({ force: true }).then(() => { + return Product.bulkCreate([{ + title: 'Chair', + User: { + first_name: 'Mick', + last_name: 'Broadstone' + } + }, { + title: 'Table', + User: { + first_name: 'John', + last_name: 'Johnson' + } + }], { + include: [{ + model: User, + myOption: 'option' + }] + }).then(savedProducts => { + expect(savedProducts[0].isIncludeCreatedOnAfterCreate).to.be.true; + expect(savedProducts[0].User.createOptions.myOption).to.be.equal('option'); + + expect(savedProducts[1].isIncludeCreatedOnAfterCreate).to.be.true; + expect(savedProducts[1].User.createOptions.myOption).to.be.equal('option'); + + return Promise.all([ + Product.findOne({ + where: { id: savedProducts[0].id }, + include: [User] + }), + Product.findOne({ + where: { id: savedProducts[1].id }, + include: [User] + }) + ]).then(persistedProducts => { + expect(persistedProducts[0].User).to.be.ok; + expect(persistedProducts[0].User.first_name).to.be.equal('Mick'); + expect(persistedProducts[0].User.last_name).to.be.equal('Broadstone'); + + expect(persistedProducts[1].User).to.be.ok; + expect(persistedProducts[1].User.first_name).to.be.equal('John'); + expect(persistedProducts[1].User.last_name).to.be.equal('Johnson'); + }); + }); + }); + }); + + it('should bulkCreate data for BelongsTo relations with no nullable FK', function() { + const Product = this.sequelize.define('Product', { + title: Sequelize.STRING + }); + const User = this.sequelize.define('User', { + first_name: Sequelize.STRING + }); + + Product.belongsTo(User, { + foreignKey: { + allowNull: false + } + }); + + return this.sequelize.sync({ force: true }).then(() => { + return Product.bulkCreate([{ + title: 'Chair', + User: { + first_name: 'Mick' + } + }, { + title: 'Table', + User: { + first_name: 'John' + } + }], { + include: [{ + model: User + }] + }).then(savedProducts => { + expect(savedProducts[0]).to.exist; + expect(savedProducts[0].title).to.be.equal('Chair'); + expect(savedProducts[0].User).to.exist; + expect(savedProducts[0].User.first_name).to.be.equal('Mick'); + + expect(savedProducts[1]).to.exist; + expect(savedProducts[1].title).to.be.equal('Table'); + expect(savedProducts[1].User).to.exist; + expect(savedProducts[1].User.first_name).to.be.equal('John'); + }); + }); + }); + + it('should bulkCreate data for BelongsTo relations with alias', function() { + const Product = this.sequelize.define('Product', { + title: Sequelize.STRING + }); + const User = this.sequelize.define('User', { + first_name: Sequelize.STRING, + last_name: Sequelize.STRING + }); + + const Creator = Product.belongsTo(User, { as: 'creator' }); + + return this.sequelize.sync({ force: true }).then(() => { + return Product.bulkCreate([{ + title: 'Chair', + creator: { + first_name: 'Matt', + last_name: 'Hansen' + } + }, { + title: 'Table', + creator: { + first_name: 'John', + last_name: 'Johnson' + } + }], { + include: [Creator] + }).then(savedProducts => { + return Promise.all([ + Product.findOne({ + where: { id: savedProducts[0].id }, + include: [Creator] + }), + Product.findOne({ + where: { id: savedProducts[1].id }, + include: [Creator] + }) + ]).then(persistedProducts => { + expect(persistedProducts[0].creator).to.be.ok; + expect(persistedProducts[0].creator.first_name).to.be.equal('Matt'); + expect(persistedProducts[0].creator.last_name).to.be.equal('Hansen'); + + expect(persistedProducts[1].creator).to.be.ok; + expect(persistedProducts[1].creator.first_name).to.be.equal('John'); + expect(persistedProducts[1].creator.last_name).to.be.equal('Johnson'); + }); + }); + }); + }); + + it('should bulkCreate data for HasMany relations', function() { + const Product = this.sequelize.define('Product', { + title: Sequelize.STRING + }, { + hooks: { + afterBulkCreate(products) { + products.forEach(product => { + product.areIncludesCreatedOnAfterCreate = product.Tags && + product.Tags.every(tag => { + return !!tag.id; + }); + }); + } + } + }); + const Tag = this.sequelize.define('Tag', { + name: Sequelize.STRING + }, { + hooks: { + afterBulkCreate(tags, options) { + tags.forEach(tag => tag.createOptions = options); + } + } + }); + + Product.hasMany(Tag); + + return this.sequelize.sync({ force: true }).then(() => { + return Product.bulkCreate([{ + id: 1, + title: 'Chair', + Tags: [ + { id: 1, name: 'Alpha' }, + { id: 2, name: 'Beta' } + ] + }, { + id: 2, + title: 'Table', + Tags: [ + { id: 3, name: 'Gamma' }, + { id: 4, name: 'Delta' } + ] + }], { + include: [{ + model: Tag, + myOption: 'option' + }] + }).then(savedProducts => { + expect(savedProducts[0].areIncludesCreatedOnAfterCreate).to.be.true; + expect(savedProducts[0].Tags[0].createOptions.myOption).to.be.equal('option'); + expect(savedProducts[0].Tags[1].createOptions.myOption).to.be.equal('option'); + + expect(savedProducts[1].areIncludesCreatedOnAfterCreate).to.be.true; + expect(savedProducts[1].Tags[0].createOptions.myOption).to.be.equal('option'); + expect(savedProducts[1].Tags[1].createOptions.myOption).to.be.equal('option'); + return Promise.all([ + Product.findOne({ + where: { id: savedProducts[0].id }, + include: [Tag] + }), + Product.findOne({ + where: { id: savedProducts[1].id }, + include: [Tag] + }) + ]).then(persistedProducts => { + expect(persistedProducts[0].Tags).to.be.ok; + expect(persistedProducts[0].Tags.length).to.equal(2); + + expect(persistedProducts[1].Tags).to.be.ok; + expect(persistedProducts[1].Tags.length).to.equal(2); + }); + }); + }); + }); + + it('should bulkCreate data for HasMany relations with alias', function() { + const Product = this.sequelize.define('Product', { + title: Sequelize.STRING + }); + const Tag = this.sequelize.define('Tag', { + name: Sequelize.STRING + }); + + const Categories = Product.hasMany(Tag, { as: 'categories' }); + + return this.sequelize.sync({ force: true }).then(() => { + return Product.bulkCreate([{ + id: 1, + title: 'Chair', + categories: [ + { id: 1, name: 'Alpha' }, + { id: 2, name: 'Beta' } + ] + }, { + id: 2, + title: 'Table', + categories: [ + { id: 3, name: 'Gamma' }, + { id: 4, name: 'Delta' } + ] + }], { + include: [Categories] + }).then(savedProducts => { + return Promise.all([ + Product.findOne({ + where: { id: savedProducts[0].id }, + include: [Categories] + }), + Product.findOne({ + where: { id: savedProducts[1].id }, + include: [Categories] + }) + ]).then(persistedProducts => { + expect(persistedProducts[0].categories).to.be.ok; + expect(persistedProducts[0].categories.length).to.equal(2); + + expect(persistedProducts[1].categories).to.be.ok; + expect(persistedProducts[1].categories.length).to.equal(2); + }); + }); + }); + }); + + it('should bulkCreate data for HasOne relations', function() { + const User = this.sequelize.define('User', { + username: Sequelize.STRING + }); + + const Task = this.sequelize.define('Task', { + title: Sequelize.STRING + }); + + User.hasOne(Task); + + return this.sequelize.sync({ force: true }).then(() => { + return User.bulkCreate([{ + username: 'Muzzy', + Task: { + title: 'Eat Clocks' + } + }, { + username: 'Walker', + Task: { + title: 'Walk' + } + }], { + include: [Task] + }).then(savedUsers => { + return Promise.all([ + User.findOne({ + where: { id: savedUsers[0].id }, + include: [Task] + }), + User.findOne({ + where: { id: savedUsers[1].id }, + include: [Task] + }) + ]).then(persistedUsers => { + expect(persistedUsers[0].Task).to.be.ok; + expect(persistedUsers[1].Task).to.be.ok; + }); + }); + }); + }); + + it('should bulkCreate data for HasOne relations with alias', function() { + const User = this.sequelize.define('User', { + username: Sequelize.STRING + }); + + const Task = this.sequelize.define('Task', { + title: Sequelize.STRING + }); + + const Job = User.hasOne(Task, { as: 'job' }); + + + return this.sequelize.sync({ force: true }).then(() => { + return User.bulkCreate([{ + username: 'Muzzy', + job: { + title: 'Eat Clocks' + } + }, { + username: 'Walker', + job: { + title: 'Walk' + } + }], { + include: [Job] + }).then(savedUsers => { + return Promise.all([ + User.findOne({ + where: { id: savedUsers[0].id }, + include: [Job] + }), + User.findOne({ + where: { id: savedUsers[1].id }, + include: [Job] + }) + ]).then(persistedUsers => { + expect(persistedUsers[0].job).to.be.ok; + expect(persistedUsers[1].job).to.be.ok; + }); + }); + }); + }); + + it('should bulkCreate data for BelongsToMany relations', function() { + const User = this.sequelize.define('User', { + username: DataTypes.STRING + }, { + hooks: { + afterBulkCreate(users) { + users.forEach(user => { + user.areIncludesCreatedOnAfterCreate = user.Tasks && + user.Tasks.every(task => { + return !!task.id; + }); + }); + } + } + }); + + const Task = this.sequelize.define('Task', { + title: DataTypes.STRING, + active: DataTypes.BOOLEAN + }, { + hooks: { + afterBulkCreate(tasks, options) { + tasks.forEach(task => { + task.createOptions = options; + }); + } + } + }); + + User.belongsToMany(Task, { through: 'user_task' }); + Task.belongsToMany(User, { through: 'user_task' }); + + return this.sequelize.sync({ force: true }).then(() => { + return User.bulkCreate([{ + username: 'John', + Tasks: [ + { title: 'Get rich', active: true }, + { title: 'Die trying', active: false } + ] + }, { + username: 'Jack', + Tasks: [ + { title: 'Prepare sandwich', active: true }, + { title: 'Each sandwich', active: false } + ] + }], { + include: [{ + model: Task, + myOption: 'option' + }] + }).then(savedUsers => { + expect(savedUsers[0].areIncludesCreatedOnAfterCreate).to.be.true; + expect(savedUsers[0].Tasks[0].createOptions.myOption).to.be.equal('option'); + expect(savedUsers[0].Tasks[1].createOptions.myOption).to.be.equal('option'); + + expect(savedUsers[1].areIncludesCreatedOnAfterCreate).to.be.true; + expect(savedUsers[1].Tasks[0].createOptions.myOption).to.be.equal('option'); + expect(savedUsers[1].Tasks[1].createOptions.myOption).to.be.equal('option'); + return Promise.all([ + User.findOne({ + where: { id: savedUsers[0].id }, + include: [Task] + }), + User.findOne({ + where: { id: savedUsers[1].id }, + include: [Task] + }) + ]).then(persistedUsers => { + expect(persistedUsers[0].Tasks).to.be.ok; + expect(persistedUsers[0].Tasks.length).to.equal(2); + + expect(persistedUsers[1].Tasks).to.be.ok; + expect(persistedUsers[1].Tasks.length).to.equal(2); + }); + }); + }); + }); + + it('should bulkCreate data for polymorphic BelongsToMany relations', function() { + const Post = this.sequelize.define('Post', { + title: DataTypes.STRING + }, { + tableName: 'posts', + underscored: true + }); + + const Tag = this.sequelize.define('Tag', { + name: DataTypes.STRING + }, { + tableName: 'tags', + underscored: true + }); + + const ItemTag = this.sequelize.define('ItemTag', { + tag_id: { + type: DataTypes.INTEGER, + references: { + model: 'tags', + key: 'id' + } + }, + taggable_id: { + type: DataTypes.INTEGER, + references: null + }, + taggable: { + type: DataTypes.STRING + } + }, { + tableName: 'item_tag', + underscored: true + }); + + Post.belongsToMany(Tag, { + as: 'tags', + foreignKey: 'taggable_id', + constraints: false, + through: { + model: ItemTag, + scope: { + taggable: 'post' + } + } + }); + + Tag.belongsToMany(Post, { + as: 'posts', + foreignKey: 'tag_id', + constraints: false, + through: { + model: ItemTag, + scope: { + taggable: 'post' + } + } + }); + + return this.sequelize.sync({ force: true }).then(() => { + return Post.bulkCreate([{ + title: 'Polymorphic Associations', + tags: [ + { + name: 'polymorphic' + }, + { + name: 'associations' + } + ] + }, { + title: 'Second Polymorphic Associations', + tags: [ + { + name: 'second polymorphic' + }, + { + name: 'second associations' + } + ] + }], { + include: [{ + model: Tag, + as: 'tags', + through: { + model: ItemTag + } + }] + } + ); + }).then(savedPosts => { + // The saved post should include the two tags + expect(savedPosts[0].tags.length).to.equal(2); + expect(savedPosts[1].tags.length).to.equal(2); + // The saved post should be able to retrieve the two tags + // using the convenience accessor methods + return Promise.all([ + savedPosts[0].getTags(), + savedPosts[1].getTags() + ]); + }).then(savedTagGroups => { + // All nested tags should be returned + expect(savedTagGroups[0].length).to.equal(2); + expect(savedTagGroups[1].length).to.equal(2); + }).then(() => { + return ItemTag.findAll(); + }).then(itemTags => { + // Four "through" models should be created + expect(itemTags.length).to.equal(4); + // And their polymorphic field should be correctly set to 'post' + expect(itemTags[0].taggable).to.equal('post'); + expect(itemTags[1].taggable).to.equal('post'); + + expect(itemTags[2].taggable).to.equal('post'); + expect(itemTags[3].taggable).to.equal('post'); + }); + }); + + it('should bulkCreate data for BelongsToMany relations with alias', function() { + const User = this.sequelize.define('User', { + username: DataTypes.STRING + }); + + const Task = this.sequelize.define('Task', { + title: DataTypes.STRING, + active: DataTypes.BOOLEAN + }); + + const Jobs = User.belongsToMany(Task, { through: 'user_job', as: 'jobs' }); + Task.belongsToMany(User, { through: 'user_job' }); + + return this.sequelize.sync({ force: true }).then(() => { + return User.bulkCreate([{ + username: 'John', + jobs: [ + { title: 'Get rich', active: true }, + { title: 'Die trying', active: false } + ] + }, { + username: 'Jack', + jobs: [ + { title: 'Prepare sandwich', active: true }, + { title: 'Eat sandwich', active: false } + ] + }], { + include: [Jobs] + }).then(savedUsers => { + return Promise.all([ + User.findOne({ + where: { id: savedUsers[0].id }, + include: [Jobs] + }), + User.findOne({ + where: { id: savedUsers[1].id }, + include: [Jobs] + }) + ]).then(persistedUsers => { + expect(persistedUsers[0].jobs).to.be.ok; + expect(persistedUsers[0].jobs.length).to.equal(2); + + expect(persistedUsers[1].jobs).to.be.ok; + expect(persistedUsers[1].jobs.length).to.equal(2); + }); + }); + }); + }); + }); + }); +});