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

Commit 81ce8ee3 by Andy Edwards Committed by GitHub

refactor(model): asyncify methods (#12122)

1 parent bccb447b
...@@ -1380,12 +1380,12 @@ class Model { ...@@ -1380,12 +1380,12 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
static drop(options) { static async drop(options) {
return this.QueryInterface.dropTable(this.getTableName(options), options); return await this.QueryInterface.dropTable(this.getTableName(options), options);
} }
static dropSchema(schema) { static async dropSchema(schema) {
return this.QueryInterface.dropSchema(schema); return await this.QueryInterface.dropSchema(schema);
} }
/** /**
...@@ -1675,7 +1675,7 @@ class Model { ...@@ -1675,7 +1675,7 @@ class Model {
* *
* @returns {Promise<Array<Model>>} * @returns {Promise<Array<Model>>}
*/ */
static findAll(options) { static async findAll(options) {
if (options !== undefined && !_.isPlainObject(options)) { if (options !== undefined && !_.isPlainObject(options)) {
throw new sequelizeErrors.QueryError('The argument passed to findAll must be an options object, use findByPk if you wish to pass a single primary key value'); throw new sequelizeErrors.QueryError('The argument passed to findAll must be an options object, use findByPk if you wish to pass a single primary key value');
} }
...@@ -1700,21 +1700,18 @@ class Model { ...@@ -1700,21 +1700,18 @@ class Model {
? options.rejectOnEmpty ? options.rejectOnEmpty
: this.options.rejectOnEmpty; : this.options.rejectOnEmpty;
return Promise.resolve().then(() => {
this._injectScope(options); this._injectScope(options);
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeFind', options); await this.runHooks('beforeFind', options);
} }
}).then(() => {
this._conformIncludes(options, this); this._conformIncludes(options, this);
this._expandAttributes(options); this._expandAttributes(options);
this._expandIncludeAll(options); this._expandIncludeAll(options);
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeFindAfterExpandIncludeAll', options); await this.runHooks('beforeFindAfterExpandIncludeAll', options);
} }
}).then(() => {
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes); options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
if (options.include) { if (options.include) {
...@@ -1747,17 +1744,13 @@ class Model { ...@@ -1747,17 +1744,13 @@ class Model {
options = this._paranoidClause(this, options); options = this._paranoidClause(this, options);
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeFindAfterOptions', options); await this.runHooks('beforeFindAfterOptions', options);
} }
}).then(() => {
const selectOptions = Object.assign({}, options, { tableNames: Object.keys(tableNames) }); const selectOptions = Object.assign({}, options, { tableNames: Object.keys(tableNames) });
return this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions); const results = await this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions);
}).then(results => {
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.runHooks('afterFind', results, options)).then(() => results); await this.runHooks('afterFind', results, options);
} }
return results;
}).then(results => {
//rejectOnEmpty mode //rejectOnEmpty mode
if (_.isEmpty(results) && options.rejectOnEmpty) { if (_.isEmpty(results) && options.rejectOnEmpty) {
...@@ -1770,8 +1763,7 @@ class Model { ...@@ -1770,8 +1763,7 @@ class Model {
throw new sequelizeErrors.EmptyResultError(); throw new sequelizeErrors.EmptyResultError();
} }
return Model._findSeparate(results, options); return await Model._findSeparate(results, options);
});
} }
static warnOnInvalidOptions(options, validColumnNames) { static warnOnInvalidOptions(options, validColumnNames) {
...@@ -1804,17 +1796,17 @@ class Model { ...@@ -1804,17 +1796,17 @@ class Model {
return attributes; return attributes;
} }
static _findSeparate(results, options) { static async _findSeparate(results, options) {
if (!options.include || options.raw || !results) return Promise.resolve(results); if (!options.include || options.raw || !results) return results;
const original = results; const original = results;
if (options.plain) results = [results]; if (options.plain) results = [results];
if (!results.length) return original; if (!results.length) return original;
return Promise.all(options.include.map(include => { await Promise.all(options.include.map(async include => {
if (!include.separate) { if (!include.separate) {
return Model._findSeparate( return await Model._findSeparate(
results.reduce((memo, result) => { results.reduce((memo, result) => {
let associations = result.get(include.association.as); let associations = result.get(include.association.as);
...@@ -1837,11 +1829,12 @@ class Model { ...@@ -1837,11 +1829,12 @@ class Model {
); );
} }
return include.association.get(results, Object.assign( const map = await include.association.get(results, Object.assign(
{}, {},
_.omit(options, nonCascadingOptions), _.omit(options, nonCascadingOptions),
_.omit(include, ['parent', 'association', 'as', 'originalAttributes']) _.omit(include, ['parent', 'association', 'as', 'originalAttributes'])
)).then(map => { ));
for (const result of results) { for (const result of results) {
result.set( result.set(
include.association.as, include.association.as,
...@@ -1849,8 +1842,9 @@ class Model { ...@@ -1849,8 +1842,9 @@ class Model {
{ raw: true } { raw: true }
); );
} }
}); }));
})).then(() => original);
return original;
} }
/** /**
...@@ -1866,10 +1860,10 @@ class Model { ...@@ -1866,10 +1860,10 @@ class Model {
* *
* @returns {Promise<Model>} * @returns {Promise<Model>}
*/ */
static findByPk(param, options) { static async findByPk(param, options) {
// return Promise resolved with null if no arguments are passed // return Promise resolved with null if no arguments are passed
if ([null, undefined].includes(param)) { if ([null, undefined].includes(param)) {
return Promise.resolve(null); return null;
} }
options = Utils.cloneDeep(options) || {}; options = Utils.cloneDeep(options) || {};
...@@ -1883,7 +1877,7 @@ class Model { ...@@ -1883,7 +1877,7 @@ class Model {
} }
// Bypass a possible overloaded findOne // Bypass a possible overloaded findOne
return this.findOne(options); return await this.findOne(options);
} }
/** /**
...@@ -1898,7 +1892,7 @@ class Model { ...@@ -1898,7 +1892,7 @@ class Model {
* *
* @returns {Promise<Model|null>} * @returns {Promise<Model|null>}
*/ */
static findOne(options) { static async findOne(options) {
if (options !== undefined && !_.isPlainObject(options)) { if (options !== undefined && !_.isPlainObject(options)) {
throw new Error('The argument passed to findOne must be an options object, use findByPk if you wish to pass a single primary key value'); throw new Error('The argument passed to findOne must be an options object, use findByPk if you wish to pass a single primary key value');
} }
...@@ -1917,7 +1911,7 @@ class Model { ...@@ -1917,7 +1911,7 @@ class Model {
} }
// Bypass a possible overloaded findAll. // Bypass a possible overloaded findAll.
return this.findAll(_.defaults(options, { return await this.findAll(_.defaults(options, {
plain: true plain: true
})); }));
} }
...@@ -1938,7 +1932,7 @@ class Model { ...@@ -1938,7 +1932,7 @@ class Model {
* *
* @returns {Promise<DataTypes|object>} Returns the aggregate result cast to `options.dataType`, unless `options.plain` is false, in which case the complete data result is returned. * @returns {Promise<DataTypes|object>} Returns the aggregate result cast to `options.dataType`, unless `options.plain` is false, in which case the complete data result is returned.
*/ */
static aggregate(attribute, aggregateFunction, options) { static async aggregate(attribute, aggregateFunction, options) {
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
// We need to preserve attributes here as the `injectScope` call would inject non aggregate columns. // We need to preserve attributes here as the `injectScope` call would inject non aggregate columns.
...@@ -1986,12 +1980,11 @@ class Model { ...@@ -1986,12 +1980,11 @@ class Model {
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
options = this._paranoidClause(this, options); options = this._paranoidClause(this, options);
return this.QueryInterface.rawSelect(this.getTableName(options), options, aggregateFunction, this).then( value => { const value = await this.QueryInterface.rawSelect(this.getTableName(options), options, aggregateFunction, this);
if (value === null) { if (value === null) {
return 0; return 0;
} }
return value; return value;
});
} }
/** /**
...@@ -2014,15 +2007,13 @@ class Model { ...@@ -2014,15 +2007,13 @@ class Model {
* *
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */
static count(options) { static async count(options) {
return Promise.resolve().then(() => {
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
options = _.defaults(options, { hooks: true }); options = _.defaults(options, { hooks: true });
options.raw = true; options.raw = true;
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeCount', options); await this.runHooks('beforeCount', options);
} }
}).then(() => {
let col = options.col || '*'; let col = options.col || '*';
if (options.include) { if (options.include) {
col = `${this.name}.${options.col || this.primaryKeyField}`; col = `${this.name}.${options.col || this.primaryKeyField}`;
...@@ -2040,8 +2031,7 @@ class Model { ...@@ -2040,8 +2031,7 @@ class Model {
options.offset = null; options.offset = null;
options.order = null; options.order = null;
return this.aggregate(col, 'count', options); return await this.aggregate(col, 'count', options);
});
} }
/** /**
...@@ -2080,7 +2070,7 @@ class Model { ...@@ -2080,7 +2070,7 @@ class Model {
* *
* @returns {Promise<{count: number, rows: Model[]}>} * @returns {Promise<{count: number, rows: Model[]}>}
*/ */
static findAndCountAll(options) { static async findAndCountAll(options) {
if (options !== undefined && !_.isPlainObject(options)) { if (options !== undefined && !_.isPlainObject(options)) {
throw new Error('The argument passed to findAndCountAll must be an options object, use findByPk if you wish to pass a single primary key value'); throw new Error('The argument passed to findAndCountAll must be an options object, use findByPk if you wish to pass a single primary key value');
} }
...@@ -2091,14 +2081,15 @@ class Model { ...@@ -2091,14 +2081,15 @@ class Model {
countOptions.attributes = undefined; countOptions.attributes = undefined;
} }
return Promise.all([ const [count, rows] = await Promise.all([
this.count(countOptions), this.count(countOptions),
this.findAll(options) this.findAll(options)
]) ]);
.then(([count, rows]) => ({
return {
count, count,
rows: count === 0 ? [] : rows rows: count === 0 ? [] : rows
})); };
} }
/** /**
...@@ -2112,8 +2103,8 @@ class Model { ...@@ -2112,8 +2103,8 @@ class Model {
* *
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
static max(field, options) { static async max(field, options) {
return this.aggregate(field, 'max', options); return await this.aggregate(field, 'max', options);
} }
/** /**
...@@ -2127,8 +2118,8 @@ class Model { ...@@ -2127,8 +2118,8 @@ class Model {
* *
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
static min(field, options) { static async min(field, options) {
return this.aggregate(field, 'min', options); return await this.aggregate(field, 'min', options);
} }
/** /**
...@@ -2142,8 +2133,8 @@ class Model { ...@@ -2142,8 +2133,8 @@ class Model {
* *
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */
static sum(field, options) { static async sum(field, options) {
return this.aggregate(field, 'sum', options); return await this.aggregate(field, 'sum', options);
} }
/** /**
...@@ -2211,10 +2202,10 @@ class Model { ...@@ -2211,10 +2202,10 @@ class Model {
* @returns {Promise<Model>} * @returns {Promise<Model>}
* *
*/ */
static create(values, options) { static async create(values, options) {
options = Utils.cloneDeep(options || {}); options = Utils.cloneDeep(options || {});
return this.build(values, { return await this.build(values, {
isNewRecord: true, isNewRecord: true,
attributes: options.fields, attributes: options.fields,
include: options.include, include: options.include,
...@@ -2234,7 +2225,7 @@ class Model { ...@@ -2234,7 +2225,7 @@ class Model {
* *
* @returns {Promise<Model,boolean>} * @returns {Promise<Model,boolean>}
*/ */
static findOrBuild(options) { static async findOrBuild(options) {
if (!options || !options.where || arguments.length > 1) { if (!options || !options.where || arguments.length > 1) {
throw new Error( throw new Error(
'Missing where attribute in the options parameter passed to findOrBuild. ' + 'Missing where attribute in the options parameter passed to findOrBuild. ' +
...@@ -2244,7 +2235,7 @@ class Model { ...@@ -2244,7 +2235,7 @@ class Model {
let values; let values;
return this.findOne(options).then(instance => { let instance = await this.findOne(options);
if (instance === null) { if (instance === null) {
values = _.clone(options.defaults) || {}; values = _.clone(options.defaults) || {};
if (_.isPlainObject(options.where)) { if (_.isPlainObject(options.where)) {
...@@ -2253,11 +2244,10 @@ class Model { ...@@ -2253,11 +2244,10 @@ class Model {
instance = this.build(values, options); instance = this.build(values, options);
return Promise.resolve([instance, true]); return [instance, true];
} }
return Promise.resolve([instance, false]); return [instance, false];
});
} }
/** /**
...@@ -2278,7 +2268,7 @@ class Model { ...@@ -2278,7 +2268,7 @@ class Model {
* *
* @returns {Promise<Model,boolean>} * @returns {Promise<Model,boolean>}
*/ */
static findOrCreate(options) { static async findOrCreate(options) {
if (!options || !options.where || arguments.length > 1) { if (!options || !options.where || arguments.length > 1) {
throw new Error( throw new Error(
'Missing where attribute in the options parameter passed to findOrCreate. ' + 'Missing where attribute in the options parameter passed to findOrCreate. ' +
...@@ -2308,15 +2298,14 @@ class Model { ...@@ -2308,15 +2298,14 @@ class Model {
let values; let values;
let transaction; let transaction;
// Create a transaction or a savepoint, depending on whether a transaction was passed in try {
return this.sequelize.transaction(options).then(t => { const t = await this.sequelize.transaction(options);
transaction = t; transaction = t;
options.transaction = t; options.transaction = t;
return this.findOne(Utils.defaults({ transaction }, options)); const found = await this.findOne(Utils.defaults({ transaction }, options));
}).then(instance => { if (found !== null) {
if (instance !== null) { return [found, false];
return [instance, false];
} }
values = _.clone(options.defaults) || {}; values = _.clone(options.defaults) || {};
...@@ -2327,14 +2316,15 @@ class Model { ...@@ -2327,14 +2316,15 @@ class Model {
options.exception = true; options.exception = true;
options.returning = true; options.returning = true;
return this.create(values, options).then(instance => { try {
if (instance.get(this.primaryKeyAttribute, { raw: true }) === null) { const created = await this.create(values, options);
if (created.get(this.primaryKeyAttribute, { raw: true }) === null) {
// If the query returned an empty result for the primary key, we know that this was actually a unique constraint violation // If the query returned an empty result for the primary key, we know that this was actually a unique constraint violation
throw new sequelizeErrors.UniqueConstraintError(); throw new sequelizeErrors.UniqueConstraintError();
} }
return [instance, true]; return [created, true];
}).catch(err => { } catch (err) {
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err; if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
const flattenedWhere = Utils.flattenObjectDeep(options.where); const flattenedWhere = Utils.flattenObjectDeep(options.where);
const flattenedWhereKeys = Object.keys(flattenedWhere).map(name => _.last(name.split('.'))); const flattenedWhereKeys = Object.keys(flattenedWhere).map(name => _.last(name.split('.')));
...@@ -2359,21 +2349,21 @@ class Model { ...@@ -2359,21 +2349,21 @@ class Model {
} }
// Someone must have created a matching instance inside the same transaction since we last did a find. Let's find it! // Someone must have created a matching instance inside the same transaction since we last did a find. Let's find it!
return this.findOne(Utils.defaults({ const otherCreated = await this.findOne(Utils.defaults({
transaction: internalTransaction ? null : transaction transaction: internalTransaction ? null : transaction
}, options)).then(instance => { }, options));
// Sanity check, ideally we caught this at the defaultFeilds/err.fields check // Sanity check, ideally we caught this at the defaultFeilds/err.fields check
// But if we didn't and instance is null, we will throw // But if we didn't and instance is null, we will throw
if (instance === null) throw err; if (otherCreated === null) throw err;
return [instance, false];
}); return [otherCreated, false];
}); }
}).finally(() => { } finally {
if (internalTransaction && transaction) { if (internalTransaction && transaction) {
// If we created a transaction internally (and not just a savepoint), we should clean it up await transaction.commit();
return transaction.commit(); }
} }
});
} }
/** /**
...@@ -2389,7 +2379,7 @@ class Model { ...@@ -2389,7 +2379,7 @@ class Model {
* *
* @returns {Promise<Model,boolean>} * @returns {Promise<Model,boolean>}
*/ */
static findCreateFind(options) { static async findCreateFind(options) {
if (!options || !options.where) { if (!options || !options.where) {
throw new Error( throw new Error(
'Missing where attribute in the options parameter passed to findCreateFind.' 'Missing where attribute in the options parameter passed to findCreateFind.'
...@@ -2402,16 +2392,17 @@ class Model { ...@@ -2402,16 +2392,17 @@ class Model {
} }
return this.findOne(options).then(result => { const found = await this.findOne(options);
if (result) return [result, false]; if (found) return [found, false];
return this.create(values, options) try {
.then(result => [result, true]) const created = await this.create(values, options);
.catch(err => { return [created, true];
} catch (err) {
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err; if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
return this.findOne(options).then(result => [result, false]); const foundAgain = await this.findOne(options);
}); return [foundAgain, false];
}); }
} }
/** /**
...@@ -2438,7 +2429,7 @@ class Model { ...@@ -2438,7 +2429,7 @@ class Model {
* *
* @returns {Promise<boolean>} Returns a boolean indicating whether the row was created or updated. For MySQL/MariaDB, it returns `true` when inserted and `false` when updated. For Postgres/MSSQL with `options.returning` true, it returns record and created boolean with signature `<Model, created>`. * @returns {Promise<boolean>} Returns a boolean indicating whether the row was created or updated. For MySQL/MariaDB, it returns `true` when inserted and `false` when updated. For Postgres/MSSQL with `options.returning` true, it returns record and created boolean with signature `<Model, created>`.
*/ */
static upsert(values, options) { static async upsert(values, options) {
options = Object.assign({ options = Object.assign({
hooks: true, hooks: true,
returning: false, returning: false,
...@@ -2457,11 +2448,9 @@ class Model { ...@@ -2457,11 +2448,9 @@ class Model {
options.fields = changed; options.fields = changed;
} }
return Promise.resolve().then(() => {
if (options.validate) { if (options.validate) {
return instance.validate(options); await instance.validate(options);
} }
}).then(() => {
// Map field names // Map field names
const updatedDataValues = _.pick(instance.dataValues, changed); const updatedDataValues = _.pick(instance.dataValues, changed);
const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this); const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this);
...@@ -2485,28 +2474,23 @@ class Model { ...@@ -2485,28 +2474,23 @@ class Model {
delete updateValues[this.primaryKeyField]; delete updateValues[this.primaryKeyField];
} }
return Promise.resolve().then(() => {
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeUpsert', values, options); await this.runHooks('beforeUpsert', values, options);
} }
}) const [created, primaryKey] = await this.QueryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), this, options);
.then(() => { let result;
return this.QueryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), this, options);
})
.then(([created, primaryKey]) => {
if (options.returning === true && primaryKey) { if (options.returning === true && primaryKey) {
return this.findByPk(primaryKey, options).then(record => [record, created]); const record = await this.findByPk(primaryKey, options);
result = [record, created];
} else {
result = created;
} }
return created;
})
.then(result => {
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.runHooks('afterUpsert', result, options)).then(() => result); await this.runHooks('afterUpsert', result, options);
return result;
} }
return result; return result;
});
});
} }
/** /**
...@@ -2534,9 +2518,9 @@ class Model { ...@@ -2534,9 +2518,9 @@ class Model {
* *
* @returns {Promise<Array<Model>>} * @returns {Promise<Array<Model>>}
*/ */
static bulkCreate(records, options = {}) { static async bulkCreate(records, options = {}) {
if (!records.length) { if (!records.length) {
return Promise.resolve([]); return [];
} }
const dialect = this.sequelize.options.dialect; const dialect = this.sequelize.options.dialect;
...@@ -2554,7 +2538,7 @@ class Model { ...@@ -2554,7 +2538,7 @@ class Model {
const instances = records.map(values => this.build(values, { isNewRecord: true, include: options.include })); const instances = records.map(values => this.build(values, { isNewRecord: true, include: options.include }));
const recursiveBulkCreate = (instances, options) => { const recursiveBulkCreate = async (instances, options) => {
options = Object.assign({ options = Object.assign({
validate: false, validate: false,
hooks: true, hooks: true,
...@@ -2571,10 +2555,10 @@ class Model { ...@@ -2571,10 +2555,10 @@ class Model {
} }
if (options.ignoreDuplicates && ['mssql'].includes(dialect)) { if (options.ignoreDuplicates && ['mssql'].includes(dialect)) {
return Promise.reject(new Error(`${dialect} does not support the ignoreDuplicates option.`)); throw new Error(`${dialect} does not support the ignoreDuplicates option.`);
} }
if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'sqlite' && dialect !== 'postgres')) { if (options.updateOnDuplicate && (dialect !== 'mysql' && dialect !== 'mariadb' && dialect !== 'sqlite' && dialect !== 'postgres')) {
return Promise.reject(new Error(`${dialect} does not support the updateOnDuplicate option.`)); throw new Error(`${dialect} does not support the updateOnDuplicate option.`);
} }
const model = options.model; const model = options.model;
...@@ -2590,36 +2574,35 @@ class Model { ...@@ -2590,36 +2574,35 @@ class Model {
options.updateOnDuplicate options.updateOnDuplicate
); );
} else { } else {
return Promise.reject(new Error('updateOnDuplicate option only supports non-empty array.')); throw new Error('updateOnDuplicate option only supports non-empty array.');
} }
} }
return Promise.resolve().then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
return model.runHooks('beforeBulkCreate', instances, options); await model.runHooks('beforeBulkCreate', instances, options);
} }
}).then(() => {
// Validate // Validate
if (options.validate) { if (options.validate) {
const errors = new Promise.AggregateError(); const errors = new Promise.AggregateError();
const validateOptions = _.clone(options); const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks; validateOptions.hooks = options.individualHooks;
return Promise.all(instances.map(instance => await Promise.all(instances.map(async instance => {
instance.validate(validateOptions).catch(err => { try {
await instance.validate(validateOptions);
} catch (err) {
errors.push(new sequelizeErrors.BulkRecordError(err, instance)); errors.push(new sequelizeErrors.BulkRecordError(err, instance));
}))).then(() => { }
}));
delete options.skip; delete options.skip;
if (errors.length) { if (errors.length) {
throw errors; throw errors;
} }
});
} }
}).then(() => {
if (options.individualHooks) { if (options.individualHooks) {
// Create each instance individually await Promise.all(instances.map(async instance => {
return Promise.all(instances.map(instance => {
const individualOptions = _.clone(options); const individualOptions = _.clone(options);
delete individualOptions.fields; delete individualOptions.fields;
delete individualOptions.individualHooks; delete individualOptions.individualHooks;
...@@ -2627,15 +2610,11 @@ class Model { ...@@ -2627,15 +2610,11 @@ class Model {
individualOptions.validate = false; individualOptions.validate = false;
individualOptions.hooks = true; individualOptions.hooks = true;
return instance.save(individualOptions); await instance.save(individualOptions);
})); }));
} } else {
if (options.include && options.include.length) {
return Promise.resolve().then(() => { await Promise.all(options.include.filter(include => include.association instanceof BelongsTo).map(async include => {
if (!options.include || !options.include.length) return;
// Nested creation for BelongsTo relations
return Promise.all(options.include.filter(include => include.association instanceof BelongsTo).map(include => {
const associationInstances = []; const associationInstances = [];
const associationInstanceIndexToInstanceMap = []; const associationInstanceIndexToInstanceMap = [];
...@@ -2658,16 +2637,16 @@ class Model { ...@@ -2658,16 +2637,16 @@ class Model {
logging: options.logging logging: options.logging
}).value(); }).value();
return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => { const createdAssociationInstances = await recursiveBulkCreate(associationInstances, includeOptions);
for (const idx in associationInstances) { for (const idx in createdAssociationInstances) {
const associationInstance = associationInstances[idx]; const associationInstance = createdAssociationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx]; const instance = associationInstanceIndexToInstanceMap[idx];
instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging }); instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging });
} }
});
})); }));
}).then(() => { }
// Create all in one query // Create all in one query
// Recreate records from instances to represent any changes made in hooks or validation // Recreate records from instances to represent any changes made in hooks or validation
records = instances.map(instance => { records = instances.map(instance => {
...@@ -2715,7 +2694,7 @@ class Model { ...@@ -2715,7 +2694,7 @@ class Model {
options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr)); options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr));
} }
return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => { const results = await model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes);
if (Array.isArray(results)) { if (Array.isArray(results)) {
results.forEach((result, i) => { results.forEach((result, i) => {
const instance = instances[i]; const instance = instances[i];
...@@ -2739,15 +2718,11 @@ class Model { ...@@ -2739,15 +2718,11 @@ class Model {
} }
}); });
} }
return results; }
});
});
}).then(() => {
if (!options.include || !options.include.length) return;
// Nested creation for HasOne/HasMany/BelongsToMany relations if (options.include && options.include.length) {
return Promise.all(options.include.filter(include => !(include.association instanceof BelongsTo || await Promise.all(options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)).map(include => { include.parent && include.parent.association instanceof BelongsToMany)).map(async include => {
const associationInstances = []; const associationInstances = [];
const associationInstanceIndexToInstanceMap = []; const associationInstanceIndexToInstanceMap = [];
...@@ -2778,12 +2753,12 @@ class Model { ...@@ -2778,12 +2753,12 @@ class Model {
logging: options.logging logging: options.logging
}).value(); }).value();
return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => { const createdAssociationInstances = await recursiveBulkCreate(associationInstances, includeOptions);
if (include.association instanceof BelongsToMany) { if (include.association instanceof BelongsToMany) {
const valueSets = []; const valueSets = [];
for (const idx in associationInstances) { for (const idx in createdAssociationInstances) {
const associationInstance = associationInstances[idx]; const associationInstance = createdAssociationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx]; const instance = associationInstanceIndexToInstanceMap[idx];
const values = {}; const values = {};
...@@ -2816,11 +2791,11 @@ class Model { ...@@ -2816,11 +2791,11 @@ class Model {
throughOptions.model = include.association.throughModel; throughOptions.model = include.association.throughModel;
const throughInstances = include.association.throughModel.bulkBuild(valueSets, throughOptions); const throughInstances = include.association.throughModel.bulkBuild(valueSets, throughOptions);
return recursiveBulkCreate(throughInstances, throughOptions); await recursiveBulkCreate(throughInstances, throughOptions);
} }
});
})); }));
}).then(() => { }
// map fields back to attributes // map fields back to attributes
instances.forEach(instance => { instances.forEach(instance => {
for (const attr in model.rawAttributes) { for (const attr in model.rawAttributes) {
...@@ -2839,12 +2814,13 @@ class Model { ...@@ -2839,12 +2814,13 @@ class Model {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return model.runHooks('afterBulkCreate', instances, options); await model.runHooks('afterBulkCreate', instances, options);
} }
}).then(() => instances);
return instances;
}; };
return recursiveBulkCreate(instances, options); return await recursiveBulkCreate(instances, options);
} }
/** /**
...@@ -2863,10 +2839,10 @@ class Model { ...@@ -2863,10 +2839,10 @@ class Model {
* @see * @see
* {@link Model.destroy} for more information * {@link Model.destroy} for more information
*/ */
static truncate(options) { static async truncate(options) {
options = Utils.cloneDeep(options) || {}; options = Utils.cloneDeep(options) || {};
options.truncate = true; options.truncate = true;
return this.destroy(options); return await this.destroy(options);
} }
/** /**
...@@ -2887,7 +2863,7 @@ class Model { ...@@ -2887,7 +2863,7 @@ class Model {
* *
* @returns {Promise<number>} The number of destroyed rows * @returns {Promise<number>} The number of destroyed rows
*/ */
static destroy(options) { static async destroy(options) {
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
this._injectScope(options); this._injectScope(options);
...@@ -2913,22 +2889,19 @@ class Model { ...@@ -2913,22 +2889,19 @@ class Model {
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
options.model = this; options.model = this;
let instances;
return Promise.resolve().then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeBulkDestroy', options); await this.runHooks('beforeBulkDestroy', options);
} }
}).then(() => { let instances;
// Get daos and run beforeDestroy hook on each record individually // Get daos and run beforeDestroy hook on each record individually
if (options.individualHooks) { if (options.individualHooks) {
return this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark }).then(value => Promise.all(value.map(instance => this.runHooks('beforeDestroy', instance, options).then(() => instance)))) instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark });
.then(_instances => {
instances = _instances; await Promise.all(instances.map(instance => this.runHooks('beforeDestroy', instance, options)));
});
} }
}).then(() => { let result;
// Run delete query (or update if paranoid) // Run delete query (or update if paranoid)
if (this._timestampAttributes.deletedAt && !options.force) { if (this._timestampAttributes.deletedAt && !options.force) {
// Set query type appropriately when running soft delete // Set query type appropriately when running soft delete
...@@ -2943,22 +2916,21 @@ class Model { ...@@ -2943,22 +2916,21 @@ class Model {
attrValueHash[field] = Utils.now(this.sequelize.options.dialect); attrValueHash[field] = Utils.now(this.sequelize.options.dialect);
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes); result = await this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes);
} else {
result = await this.QueryInterface.bulkDelete(this.getTableName(options), options.where, options, this);
} }
return this.QueryInterface.bulkDelete(this.getTableName(options), options.where, options, this);
}).then(result => {
// Run afterDestroy hook on each record individually // Run afterDestroy hook on each record individually
if (options.individualHooks) { if (options.individualHooks) {
return Promise.resolve(Promise.all(instances.map(instance => this.runHooks('afterDestroy', instance, options)))).then(() => result); await Promise.all(
instances.map(instance => this.runHooks('afterDestroy', instance, options))
);
} }
return result;
}).then(result => {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.runHooks('afterBulkDestroy', options)).then(() => result); await this.runHooks('afterBulkDestroy', options);
} }
return result; return result;
});
} }
/** /**
...@@ -2975,7 +2947,7 @@ class Model { ...@@ -2975,7 +2947,7 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
static restore(options) { static async restore(options) {
if (!this._timestampAttributes.deletedAt) throw new Error('Model is not paranoid'); if (!this._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Object.assign({ options = Object.assign({
...@@ -2986,24 +2958,20 @@ class Model { ...@@ -2986,24 +2958,20 @@ class Model {
options.type = QueryTypes.RAW; options.type = QueryTypes.RAW;
options.model = this; options.model = this;
let instances;
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
return Promise.resolve().then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
return this.runHooks('beforeBulkRestore', options); await this.runHooks('beforeBulkRestore', options);
} }
}).then(() => {
let instances;
// Get daos and run beforeRestore hook on each record individually // Get daos and run beforeRestore hook on each record individually
if (options.individualHooks) { if (options.individualHooks) {
return this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark, paranoid: false }).then(value => Promise.all(value.map(instance => this.runHooks('beforeRestore', instance, options).then(() => instance)))) instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark, paranoid: false });
.then(_instances => {
instances = _instances; await Promise.all(instances.map(instance => this.runHooks('beforeRestore', instance, options)));
});
} }
}).then(() => {
// Run undelete query // Run undelete query
const attrValueHash = {}; const attrValueHash = {};
const deletedAtCol = this._timestampAttributes.deletedAt; const deletedAtCol = this._timestampAttributes.deletedAt;
...@@ -3012,20 +2980,18 @@ class Model { ...@@ -3012,20 +2980,18 @@ class Model {
attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue; attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue;
options.omitNull = false; options.omitNull = false;
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes); const result = await this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes);
}).then(result => {
// Run afterDestroy hook on each record individually // Run afterDestroy hook on each record individually
if (options.individualHooks) { if (options.individualHooks) {
return Promise.resolve(Promise.all(instances.map(instance => this.runHooks('afterRestore', instance, options)))).then(() => result); await Promise.all(
instances.map(instance => this.runHooks('afterRestore', instance, options))
);
} }
return result;
}).then(result => {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.runHooks('afterBulkRestore', options)).then(() => result); await this.runHooks('afterBulkRestore', options);
} }
return result; return result;
});
} }
/** /**
...@@ -3051,7 +3017,7 @@ class Model { ...@@ -3051,7 +3017,7 @@ class Model {
* of affected rows, while the second element is the actual affected rows (only supported in postgres with `options.returning` true). * of affected rows, while the second element is the actual affected rows (only supported in postgres with `options.returning` true).
* *
*/ */
static update(values, options) { static async update(values, options) {
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
this._injectScope(options); this._injectScope(options);
...@@ -3092,10 +3058,7 @@ class Model { ...@@ -3092,10 +3058,7 @@ class Model {
options.model = this; options.model = this;
let instances;
let valuesUse; let valuesUse;
return Promise.resolve().then(() => {
// Validate // Validate
if (options.validate) { if (options.validate) {
const build = this.build(values); const build = this.build(values);
...@@ -3108,47 +3071,41 @@ class Model { ...@@ -3108,47 +3071,41 @@ class Model {
// We want to skip validations for all other fields // We want to skip validations for all other fields
options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values)); options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values));
return build.validate(options).then(attributes => { const attributes = await build.validate(options);
options.skip = undefined; options.skip = undefined;
if (attributes && attributes.dataValues) { if (attributes && attributes.dataValues) {
values = _.pick(attributes.dataValues, Object.keys(values)); values = _.pick(attributes.dataValues, Object.keys(values));
} }
});
} }
return null;
}).then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
options.attributes = values; options.attributes = values;
return this.runHooks('beforeBulkUpdate', options).then(() => { await this.runHooks('beforeBulkUpdate', options);
values = options.attributes; values = options.attributes;
delete options.attributes; delete options.attributes;
});
} }
return null;
}).then(() => {
valuesUse = values; valuesUse = values;
// Get instances and run beforeUpdate hook on each record individually // Get instances and run beforeUpdate hook on each record individually
let instances;
let updateDoneRowByRow = false;
if (options.individualHooks) { if (options.individualHooks) {
return this.findAll({ instances = await this.findAll({
where: options.where, where: options.where,
transaction: options.transaction, transaction: options.transaction,
logging: options.logging, logging: options.logging,
benchmark: options.benchmark, benchmark: options.benchmark,
paranoid: options.paranoid paranoid: options.paranoid
}).then(_instances => { });
instances = _instances;
if (!instances.length) {
return [];
}
if (instances.length) {
// Run beforeUpdate hooks on each record and check whether beforeUpdate hook changes values uniformly // Run beforeUpdate hooks on each record and check whether beforeUpdate hook changes values uniformly
// i.e. whether they change values for each record in the same way // i.e. whether they change values for each record in the same way
let changedValues; let changedValues;
let different = false; let different = false;
return Promise.all(instances.map(instance => { instances = await Promise.all(instances.map(async instance => {
// Record updates in instances dataValues // Record updates in instances dataValues
Object.assign(instance.dataValues, values); Object.assign(instance.dataValues, values);
// Set the changed fields on the instance // Set the changed fields on the instance
...@@ -3159,7 +3116,7 @@ class Model { ...@@ -3159,7 +3116,7 @@ class Model {
}); });
// Run beforeUpdate hook // Run beforeUpdate hook
return this.runHooks('beforeUpdate', instance, options).then(() => { await this.runHooks('beforeUpdate', instance, options);
if (!different) { if (!different) {
const thisChangedValues = {}; const thisChangedValues = {};
_.forIn(instance.dataValues, (newValue, attr) => { _.forIn(instance.dataValues, (newValue, attr) => {
...@@ -3176,9 +3133,7 @@ class Model { ...@@ -3176,9 +3133,7 @@ class Model {
} }
return instance; return instance;
}); }));
})).then(_instances => {
instances = _instances;
if (!different) { if (!different) {
const keys = Object.keys(changedValues); const keys = Object.keys(changedValues);
...@@ -3188,70 +3143,51 @@ class Model { ...@@ -3188,70 +3143,51 @@ class Model {
valuesUse = changedValues; valuesUse = changedValues;
options.fields = _.union(options.fields, keys); options.fields = _.union(options.fields, keys);
} }
return; } else {
} instances = await Promise.all(instances.map(async instance => {
// Hooks change values in a different way for each record
// Do not run original query but save each record individually
return Promise.all(instances.map(instance => {
const individualOptions = _.clone(options); const individualOptions = _.clone(options);
delete individualOptions.individualHooks; delete individualOptions.individualHooks;
individualOptions.hooks = false; individualOptions.hooks = false;
individualOptions.validate = false; individualOptions.validate = false;
return instance.save(individualOptions); return instance.save(individualOptions);
})).then(_instances => { }));
instances = _instances; updateDoneRowByRow = true;
return _instances;
});
});
});
} }
}).then(results => {
// Update already done row-by-row - exit
if (results) {
return [results.length, results];
} }
// only updatedAt is being passed, then skip update
if (
_.isEmpty(valuesUse)
|| Object.keys(valuesUse).length === 1 && valuesUse[this._timestampAttributes.updatedAt]
) {
return [0];
} }
let result;
if (updateDoneRowByRow) {
result = [instances.length, instances];
} else if (_.isEmpty(valuesUse)
|| Object.keys(valuesUse).length === 1 && valuesUse[this._timestampAttributes.updatedAt]) {
// only updatedAt is being passed, then skip update
result = [0];
} else {
valuesUse = Utils.mapValueFieldNames(valuesUse, options.fields, this); valuesUse = Utils.mapValueFieldNames(valuesUse, options.fields, this);
options = Utils.mapOptionFieldNames(options, this); options = Utils.mapOptionFieldNames(options, this);
options.hasTrigger = this.options ? this.options.hasTrigger : false; options.hasTrigger = this.options ? this.options.hasTrigger : false;
// Run query to update all rows const affectedRows = await this.QueryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, this.tableAttributes);
return this.QueryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, this.tableAttributes).then(affectedRows => {
if (options.returning) { if (options.returning) {
result = [affectedRows.length, affectedRows];
instances = affectedRows; instances = affectedRows;
return [affectedRows.length, affectedRows]; } else {
result = [affectedRows];
}
} }
return [affectedRows];
});
}).then(result => {
if (options.individualHooks) { if (options.individualHooks) {
return Promise.resolve(Promise.all(instances.map(instance => { await Promise.all(instances.map(instance => this.runHooks('afterUpdate', instance, options)));
return Promise.resolve(this.runHooks('afterUpdate', instance, options)).then(() => result);
})).then(() => {
result[1] = instances; result[1] = instances;
})).then(() => result);
} }
return result;
}).then(result => {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
options.attributes = values; options.attributes = values;
return Promise.resolve(this.runHooks('afterBulkUpdate', options).then(() => { await this.runHooks('afterBulkUpdate', options);
delete options.attributes; delete options.attributes;
})).then(() => result);
} }
return result; return result;
});
} }
/** /**
...@@ -3262,8 +3198,8 @@ class Model { ...@@ -3262,8 +3198,8 @@ class Model {
* *
* @returns {Promise} hash of attributes and their types * @returns {Promise} hash of attributes and their types
*/ */
static describe(schema, options) { static async describe(schema, options) {
return this.QueryInterface.describeTable(this.tableName, Object.assign({ schema: schema || this._schema || undefined }, options)); return await this.QueryInterface.describeTable(this.tableName, Object.assign({ schema: schema || this._schema || undefined }, options));
} }
static _getDefaultTimestamp(attr) { static _getDefaultTimestamp(attr) {
...@@ -3336,7 +3272,7 @@ class Model { ...@@ -3336,7 +3272,7 @@ class Model {
* *
* @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect * @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect
*/ */
static increment(fields, options) { static async increment(fields, options) {
options = options || {}; options = options || {};
if (typeof fields === 'string') fields = [fields]; if (typeof fields === 'string') fields = [fields];
if (Array.isArray(fields)) { if (Array.isArray(fields)) {
...@@ -3392,24 +3328,22 @@ class Model { ...@@ -3392,24 +3328,22 @@ class Model {
} }
const tableName = this.getTableName(options); const tableName = this.getTableName(options);
let promise; let affectedRows;
if (isSubtraction) { if (isSubtraction) {
promise = this.QueryInterface.decrement( affectedRows = await this.QueryInterface.decrement(
this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options
); );
} else { } else {
promise = this.QueryInterface.increment( affectedRows = await this.QueryInterface.increment(
this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options
); );
} }
return promise.then(affectedRows => {
if (options.returning) { if (options.returning) {
return [affectedRows, affectedRows.length]; return [affectedRows, affectedRows.length];
} }
return [affectedRows]; return [affectedRows];
});
} }
/** /**
...@@ -3437,12 +3371,12 @@ class Model { ...@@ -3437,12 +3371,12 @@ class Model {
* *
* @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect * @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect
*/ */
static decrement(fields, options) { static async decrement(fields, options) {
options = _.defaults({ increment: false }, options, { options = _.defaults({ increment: false }, options, {
by: 1 by: 1
}); });
return this.increment(fields, options); return await this.increment(fields, options);
} }
static _optionsMustContainWhere(options) { static _optionsMustContainWhere(options) {
...@@ -3863,7 +3797,7 @@ class Model { ...@@ -3863,7 +3797,7 @@ class Model {
* *
* @returns {Promise<Model>} * @returns {Promise<Model>}
*/ */
save(options) { async save(options) {
if (arguments.length > 1) { if (arguments.length > 1) {
throw new Error('The second argument was removed in favor of the options object.'); throw new Error('The second argument was removed in favor of the options object.');
} }
...@@ -3938,12 +3872,10 @@ class Model { ...@@ -3938,12 +3872,10 @@ class Model {
this.dataValues[createdAtAttr] = this.constructor._getDefaultTimestamp(createdAtAttr) || now; this.dataValues[createdAtAttr] = this.constructor._getDefaultTimestamp(createdAtAttr) || now;
} }
return Promise.resolve().then(() => {
// Validate // Validate
if (options.validate) { if (options.validate) {
return this.validate(options); await this.validate(options);
} }
}).then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
const beforeHookValues = _.pick(this.dataValues, options.fields); const beforeHookValues = _.pick(this.dataValues, options.fields);
...@@ -3955,8 +3887,7 @@ class Model { ...@@ -3955,8 +3887,7 @@ class Model {
ignoreChanged = _.without(ignoreChanged, updatedAtAttr); ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
} }
return this.constructor.runHooks(`before${hook}`, this, options) await this.constructor.runHooks(`before${hook}`, this, options);
.then(() => {
if (options.defaultFields && !this.isNewRecord) { if (options.defaultFields && !this.isNewRecord) {
afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged)); afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged));
...@@ -3975,22 +3906,15 @@ class Model { ...@@ -3975,22 +3906,15 @@ class Model {
// Validate again // Validate again
options.skip = _.difference(Object.keys(this.constructor.rawAttributes), hookChanged); options.skip = _.difference(Object.keys(this.constructor.rawAttributes), hookChanged);
return this.validate(options).then(() => { await this.validate(options);
delete options.skip; delete options.skip;
});
} }
} }
});
} }
}).then(() => { if (options.fields.length && this.isNewRecord && this._options.include && this._options.include.length) {
if (!options.fields.length) return this; await Promise.all(this._options.include.filter(include => include.association instanceof BelongsTo).map(async include => {
if (!this.isNewRecord) return this;
if (!this._options.include || !this._options.include.length) return this;
// Nested creation for BelongsTo relations
return Promise.all(this._options.include.filter(include => include.association instanceof BelongsTo).map(include => {
const instance = this.get(include.as); const instance = this.get(include.as);
if (!instance) return Promise.resolve(); if (!instance) return;
const includeOptions = _(Utils.cloneDeep(include)) const includeOptions = _(Utils.cloneDeep(include))
.omit(['association']) .omit(['association'])
...@@ -4000,9 +3924,11 @@ class Model { ...@@ -4000,9 +3924,11 @@ class Model {
parentRecord: this parentRecord: this
}).value(); }).value();
return instance.save(includeOptions).then(() => this[include.association.accessors.set](instance, { save: false, logging: options.logging })); await instance.save(includeOptions);
await this[include.association.accessors.set](instance, { save: false, logging: options.logging });
})); }));
}).then(() => { }
const realFields = options.fields.filter(field => !this.constructor._virtualAttributes.has(field)); const realFields = options.fields.filter(field => !this.constructor._virtualAttributes.has(field));
if (!realFields.length) return this; if (!realFields.length) return this;
if (!this.changed() && !this.isNewRecord) return this; if (!this.changed() && !this.isNewRecord) return this;
...@@ -4025,8 +3951,7 @@ class Model { ...@@ -4025,8 +3951,7 @@ class Model {
args = [this, this.constructor.getTableName(options), values, where, options]; args = [this, this.constructor.getTableName(options), values, where, options];
} }
return this.constructor.QueryInterface[query](...args) const [result, rowsUpdated] = await this.constructor.QueryInterface[query](...args);
.then(([result, rowsUpdated])=> {
if (versionAttr) { if (versionAttr) {
// Check to see that a row was updated, otherwise it's an optimistic locking error. // Check to see that a row was updated, otherwise it's an optimistic locking error.
if (rowsUpdated < 1) { if (rowsUpdated < 1) {
...@@ -4053,20 +3978,14 @@ class Model { ...@@ -4053,20 +3978,14 @@ class Model {
values = Object.assign(values, result.dataValues); values = Object.assign(values, result.dataValues);
result.dataValues = Object.assign(result.dataValues, values); result.dataValues = Object.assign(result.dataValues, values);
return result; if (wasNewRecord && this._options.include && this._options.include.length) {
}) await Promise.all(
.then(result => { this._options.include.filter(include => !(include.association instanceof BelongsTo ||
if (!wasNewRecord) return Promise.resolve(this).then(() => result); include.parent && include.parent.association instanceof BelongsToMany)).map(async include => {
if (!this._options.include || !this._options.include.length) return Promise.resolve(this).then(() => result);
// Nested creation for HasOne/HasMany/BelongsToMany relations
return Promise.resolve(Promise.all(this._options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)).map(include => {
let instances = this.get(include.as); let instances = this.get(include.as);
if (!instances) return Promise.resolve(Promise.resolve()).then(() => result); if (!instances) return;
if (!Array.isArray(instances)) instances = [instances]; if (!Array.isArray(instances)) instances = [instances];
if (!instances.length) return Promise.resolve(Promise.resolve()).then(() => result);
const includeOptions = _(Utils.cloneDeep(include)) const includeOptions = _(Utils.cloneDeep(include))
.omit(['association']) .omit(['association'])
...@@ -4077,15 +3996,15 @@ class Model { ...@@ -4077,15 +3996,15 @@ class Model {
}).value(); }).value();
// Instances will be updated in place so we can safely treat HasOne like a HasMany // Instances will be updated in place so we can safely treat HasOne like a HasMany
return Promise.resolve(Promise.all(instances.map(instance => { await Promise.all(instances.map(async instance => {
if (include.association instanceof BelongsToMany) { if (include.association instanceof BelongsToMany) {
return Promise.resolve(instance.save(includeOptions).then(() => { await instance.save(includeOptions);
const values = {}; const values0 = {};
values[include.association.foreignKey] = this.get(this.constructor.primaryKeyAttribute, { raw: true }); values0[include.association.foreignKey] = this.get(this.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true }); values0[include.association.otherKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association // Include values defined in the association
Object.assign(values, include.association.through.scope); Object.assign(values0, include.association.through.scope);
if (instance[include.association.through.model.name]) { if (instance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) { for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated || if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
...@@ -4094,35 +4013,31 @@ class Model { ...@@ -4094,35 +4013,31 @@ class Model {
typeof instance[include.association.through.model.name][attr] === undefined) { typeof instance[include.association.through.model.name][attr] === undefined) {
continue; continue;
} }
values[attr] = instance[include.association.through.model.name][attr]; values0[attr] = instance[include.association.through.model.name][attr];
} }
} }
return Promise.resolve(include.association.throughModel.create(values, includeOptions)).then(() => result); await include.association.throughModel.create(values0, includeOptions);
})).then(() => result); } else {
}
instance.set(include.association.foreignKey, this.get(include.association.sourceKey || this.constructor.primaryKeyAttribute, { raw: true }), { raw: true }); instance.set(include.association.foreignKey, this.get(include.association.sourceKey || this.constructor.primaryKeyAttribute, { raw: true }), { raw: true });
Object.assign(instance, include.association.scope); Object.assign(instance, include.association.scope);
return Promise.resolve(instance.save(includeOptions)).then(() => result); await instance.save(includeOptions);
}))).then(() => result); }
}))).then(() => result); }));
}) })
.then(result => { );
}
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.constructor.runHooks(`after${hook}`, result, options)).then(() => result); await this.constructor.runHooks(`after${hook}`, result, options);
} }
return result;
})
.then(result => {
for (const field of options.fields) { for (const field of options.fields) {
result._previousDataValues[field] = result.dataValues[field]; result._previousDataValues[field] = result.dataValues[field];
this.changed(field, false); this.changed(field, false);
} }
this.isNewRecord = false; this.isNewRecord = false;
return result; return result;
});
});
} }
/** /**
...@@ -4138,31 +4053,27 @@ class Model { ...@@ -4138,31 +4053,27 @@ class Model {
* *
* @returns {Promise<Model>} * @returns {Promise<Model>}
*/ */
reload(options) { async reload(options) {
options = Utils.defaults({}, options, { options = Utils.defaults({}, options, {
where: this.where(), where: this.where(),
include: this._options.include || null include: this._options.include || null
}); });
return this.constructor.findOne(options) const reloaded = await this.constructor.findOne(options);
.then(reload => { if (!reloaded) {
if (!reload) {
throw new sequelizeErrors.InstanceError( throw new sequelizeErrors.InstanceError(
'Instance could not be reloaded because it does not exist anymore (find call returned null)' 'Instance could not be reloaded because it does not exist anymore (find call returned null)'
); );
} }
return reload;
})
.then(reload => {
// update the internal options of the instance // update the internal options of the instance
this._options = reload._options; this._options = reloaded._options;
// re-set instance values // re-set instance values
this.set(reload.dataValues, { this.set(reloaded.dataValues, {
raw: true, raw: true,
reset: true && !options.attributes reset: true && !options.attributes
}); });
return this; return this;
});
} }
/** /**
...@@ -4177,8 +4088,8 @@ class Model { ...@@ -4177,8 +4088,8 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
validate(options) { async validate(options) {
return new InstanceValidator(this, options).validate(); return await new InstanceValidator(this, options).validate();
} }
/** /**
...@@ -4195,7 +4106,7 @@ class Model { ...@@ -4195,7 +4106,7 @@ class Model {
* *
* @returns {Promise<Model>} * @returns {Promise<Model>}
*/ */
update(values, options) { async update(values, options) {
// Clone values so it doesn't get modified for caller scope and ignore undefined values // Clone values so it doesn't get modified for caller scope and ignore undefined values
values = _.omitBy(values, value => value === undefined); values = _.omitBy(values, value => value === undefined);
...@@ -4218,7 +4129,7 @@ class Model { ...@@ -4218,7 +4129,7 @@ class Model {
options.defaultFields = options.fields; options.defaultFields = options.fields;
} }
return this.save(options); return await this.save(options);
} }
/** /**
...@@ -4232,20 +4143,19 @@ class Model { ...@@ -4232,20 +4143,19 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
destroy(options) { async destroy(options) {
options = Object.assign({ options = Object.assign({
hooks: true, hooks: true,
force: false force: false
}, options); }, options);
return Promise.resolve().then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
return this.constructor.runHooks('beforeDestroy', this, options); await this.constructor.runHooks('beforeDestroy', this, options);
} }
}).then(() => {
const where = this.where(true); const where = this.where(true);
let result;
if (this.constructor._timestampAttributes.deletedAt && options.force === false) { if (this.constructor._timestampAttributes.deletedAt && options.force === false) {
const attributeName = this.constructor._timestampAttributes.deletedAt; const attributeName = this.constructor._timestampAttributes.deletedAt;
const attribute = this.constructor.rawAttributes[attributeName]; const attribute = this.constructor.rawAttributes[attributeName];
...@@ -4259,16 +4169,15 @@ class Model { ...@@ -4259,16 +4169,15 @@ class Model {
this.setDataValue(attributeName, new Date()); this.setDataValue(attributeName, new Date());
} }
return this.save(_.defaults({ hooks: false }, options)); result = await this.save(_.defaults({ hooks: false }, options));
} else {
result = await this.constructor.QueryInterface.delete(this, this.constructor.getTableName(options), where, Object.assign({ type: QueryTypes.DELETE, limit: null }, options));
} }
return this.constructor.QueryInterface.delete(this, this.constructor.getTableName(options), where, Object.assign({ type: QueryTypes.DELETE, limit: null }, options));
}).then(result => {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.constructor.runHooks('afterDestroy', this, options)).then(() => result); await this.constructor.runHooks('afterDestroy', this, options);
} }
return result; return result;
});
} }
/** /**
...@@ -4300,7 +4209,7 @@ class Model { ...@@ -4300,7 +4209,7 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
restore(options) { async restore(options) {
if (!this.constructor._timestampAttributes.deletedAt) throw new Error('Model is not paranoid'); if (!this.constructor._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Object.assign({ options = Object.assign({
...@@ -4308,25 +4217,22 @@ class Model { ...@@ -4308,25 +4217,22 @@ class Model {
force: false force: false
}, options); }, options);
return Promise.resolve().then(() => {
// Run before hook // Run before hook
if (options.hooks) { if (options.hooks) {
return this.constructor.runHooks('beforeRestore', this, options); await this.constructor.runHooks('beforeRestore', this, options);
} }
}).then(() => {
const deletedAtCol = this.constructor._timestampAttributes.deletedAt; const deletedAtCol = this.constructor._timestampAttributes.deletedAt;
const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol]; const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
this.setDataValue(deletedAtCol, deletedAtDefaultValue); this.setDataValue(deletedAtCol, deletedAtDefaultValue);
return this.save(Object.assign({}, options, { hooks: false, omitNull: false })); const result = await this.save(Object.assign({}, options, { hooks: false, omitNull: false }));
}).then(result => {
// Run after hook // Run after hook
if (options.hooks) { if (options.hooks) {
return Promise.resolve(this.constructor.runHooks('afterRestore', this, options)).then(() => result); await this.constructor.runHooks('afterRestore', this, options);
return result;
} }
return result; return result;
});
} }
/** /**
...@@ -4360,14 +4266,16 @@ class Model { ...@@ -4360,14 +4266,16 @@ class Model {
* @returns {Promise<Model>} * @returns {Promise<Model>}
* @since 4.0.0 * @since 4.0.0
*/ */
increment(fields, options) { async increment(fields, options) {
const identifier = this.where(); const identifier = this.where();
options = Utils.cloneDeep(options); options = Utils.cloneDeep(options);
options.where = Object.assign({}, options.where, identifier); options.where = Object.assign({}, options.where, identifier);
options.instance = this; options.instance = this;
return this.constructor.increment(fields, options).then(() => this); await this.constructor.increment(fields, options);
return this;
} }
/** /**
...@@ -4399,12 +4307,12 @@ class Model { ...@@ -4399,12 +4307,12 @@ class Model {
* *
* @returns {Promise} * @returns {Promise}
*/ */
decrement(fields, options) { async decrement(fields, options) {
options = _.defaults({ increment: false }, options, { options = _.defaults({ increment: false }, options, {
by: 1 by: 1
}); });
return this.increment(fields, options); return await this.increment(fields, options);
} }
/** /**
......
...@@ -651,10 +651,9 @@ describe(Support.getTestDialectTeaser('Instance'), () => { ...@@ -651,10 +651,9 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
}); });
describe('restore', () => { describe('restore', () => {
it('returns an error if the model is not paranoid', function() { it('returns an error if the model is not paranoid', async function() {
return this.User.create({ username: 'Peter', secretValue: '42' }).then(user => { const user = await this.User.create({ username: 'Peter', secretValue: '42' });
expect(() => {user.restore();}).to.throw(Error, 'Model is not paranoid'); await expect(user.restore()).to.be.rejectedWith(Error, 'Model is not paranoid');
});
}); });
it('restores a previously deleted model', function() { it('restores a previously deleted model', function() {
......
...@@ -1555,11 +1555,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -1555,11 +1555,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
describe('restore', () => { describe('restore', () => {
it('synchronously throws an error if the model is not paranoid', async function() { it('rejects with an error if the model is not paranoid', async function() {
expect(() => { await expect(this.User.restore({ where: { secretValue: '42' } })).to.be.rejectedWith(Error, 'Model is not paranoid');
this.User.restore({ where: { secretValue: '42' } });
throw new Error('Did not throw synchronously');
}).to.throw(Error, 'Model is not paranoid');
}); });
it('restores a previously deleted model', async function() { it('restores a previously deleted model', async function() {
......
...@@ -18,14 +18,14 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -18,14 +18,14 @@ describe(Support.getTestDialectTeaser('Model'), () => {
count: Sequelize.BIGINT count: Sequelize.BIGINT
}); });
it('should reject if options are missing', () => { it('should reject if options are missing', async () => {
return expect(() => Model.increment(['id', 'count'])) await expect(Model.increment(['id', 'count']))
.to.throw('Missing where attribute in the options parameter'); .to.be.rejectedWith('Missing where attribute in the options parameter');
}); });
it('should reject if options.where are missing', () => { it('should reject if options.where are missing', async () => {
return expect(() => Model.increment(['id', 'count'], { by: 10 })) await expect(Model.increment(['id', 'count'], { by: 10 }))
.to.throw('Missing where attribute in the options parameter'); .to.be.rejectedWith('Missing where attribute in the options parameter');
}); });
}); });
}); });
......
...@@ -9,15 +9,13 @@ const chai = require('chai'), ...@@ -9,15 +9,13 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('Instance'), () => { describe(Support.getTestDialectTeaser('Instance'), () => {
describe('save', () => { describe('save', () => {
it('should disallow saves if no primary key values is present', () => { it('should disallow saves if no primary key values is present', async () => {
const Model = current.define('User', { const Model = current.define('User', {
}), }),
instance = Model.build({}, { isNewRecord: false }); instance = Model.build({}, { isNewRecord: false });
expect(() => { await expect(instance.save()).to.be.rejected;
instance.save();
}).to.throw();
}); });
describe('options tests', () => { describe('options tests', () => {
......
...@@ -35,13 +35,10 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -35,13 +35,10 @@ describe(Support.getTestDialectTeaser('Model'), () => {
this.stubDelete.restore(); this.stubDelete.restore();
}); });
it('can detect complex objects', () => { it('can detect complex objects', async () => {
const Where = function() { this.secretValue = '1'; }; const Where = function() { this.secretValue = '1'; };
expect(() => { await expect(User.destroy({ where: new Where() })).to.be.rejected;
User.destroy({ where: new Where() });
}).to.throw();
}); });
}); });
}); });
...@@ -65,9 +65,8 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -65,9 +65,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
expect(this.warnOnInvalidOptionsStub.calledOnce).to.equal(true); expect(this.warnOnInvalidOptionsStub.calledOnce).to.equal(true);
}); });
it('Throws an error when the attributes option is formatted incorrectly', () => { it('Throws an error when the attributes option is formatted incorrectly', async () => {
const errorFunction = Model.findAll.bind(Model, { attributes: 'name' }); await expect(Model.findAll({ attributes: 'name' })).to.be.rejectedWith(sequelizeErrors.QueryError);
expect(errorFunction).to.throw(sequelizeErrors.QueryError);
}); });
}); });
......
...@@ -46,12 +46,10 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -46,12 +46,10 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
it('can detect complexe objects', function() { it('can detect complexe objects', async function() {
const Where = function() { this.secretValue = '1'; }; const Where = function() { this.secretValue = '1'; };
expect(() => { await expect(this.User.update(this.updates, { where: new Where() })).to.be.rejected;
this.User.update(this.updates, { where: new Where() });
}).to.throw();
}); });
}); });
}); });
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!