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

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,78 +1700,70 @@ class Model { ...@@ -1700,78 +1700,70 @@ 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) {
options.hasJoin = true; options.hasJoin = true;
this._validateIncludedElements(options, tableNames); this._validateIncludedElements(options, tableNames);
// If we're not raw, we have to make sure we include the primary key for de-duplication // If we're not raw, we have to make sure we include the primary key for de-duplication
if ( if (
options.attributes options.attributes
&& !options.raw && !options.raw
&& this.primaryKeyAttribute && this.primaryKeyAttribute
&& !options.attributes.includes(this.primaryKeyAttribute) && !options.attributes.includes(this.primaryKeyAttribute)
&& (!options.group || !options.hasSingleAssociation || options.hasMultiAssociation) && (!options.group || !options.hasSingleAssociation || options.hasMultiAssociation)
) { ) {
options.attributes = [this.primaryKeyAttribute].concat(options.attributes); options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
}
} }
}
if (!options.attributes) { if (!options.attributes) {
options.attributes = Object.keys(this.rawAttributes); options.attributes = Object.keys(this.rawAttributes);
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes); options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
} }
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
this.options.whereCollection = options.where || null; this.options.whereCollection = options.where || null;
Utils.mapFinderOptions(options, this); Utils.mapFinderOptions(options, this);
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) }); const results = await this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions);
return this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions); if (options.hooks) {
}).then(results => { await this.runHooks('afterFind', results, options);
if (options.hooks) { }
return Promise.resolve(this.runHooks('afterFind', results, options)).then(() => results);
}
return results;
}).then(results => {
//rejectOnEmpty mode //rejectOnEmpty mode
if (_.isEmpty(results) && options.rejectOnEmpty) { if (_.isEmpty(results) && options.rejectOnEmpty) {
if (typeof options.rejectOnEmpty === 'function') { if (typeof options.rejectOnEmpty === 'function') {
throw new options.rejectOnEmpty(); throw new options.rejectOnEmpty();
}
if (typeof options.rejectOnEmpty === 'object') {
throw options.rejectOnEmpty;
}
throw new sequelizeErrors.EmptyResultError();
} }
if (typeof options.rejectOnEmpty === 'object') {
throw options.rejectOnEmpty;
}
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,20 +1829,22 @@ class Model { ...@@ -1837,20 +1829,22 @@ 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) {
result.set( for (const result of results) {
include.association.as, result.set(
map[result.get(include.association.sourceKey)], include.association.as,
{ raw: true } map[result.get(include.association.sourceKey)],
); { 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,34 +2007,31 @@ class Model { ...@@ -2014,34 +2007,31 @@ 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) { await this.runHooks('beforeCount', options);
return this.runHooks('beforeCount', options); }
} let col = options.col || '*';
}).then(() => { if (options.include) {
let col = options.col || '*'; col = `${this.name}.${options.col || this.primaryKeyField}`;
if (options.include) { }
col = `${this.name}.${options.col || this.primaryKeyField}`; if (options.distinct && col === '*') {
} col = this.primaryKeyField;
if (options.distinct && col === '*') { }
col = this.primaryKeyField; options.plain = !options.group;
} options.dataType = new DataTypes.INTEGER();
options.plain = !options.group; options.includeIgnoreAttributes = false;
options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false;
// No limit, offset or order for the options max be given to count() // No limit, offset or order for the options max be given to count()
// Set them to null to prevent scopes setting those values // Set them to null to prevent scopes setting those values
options.limit = null; options.limit = null;
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]) => ({
count, return {
rows: count === 0 ? [] : rows count,
})); 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,20 +2235,19 @@ class Model { ...@@ -2244,20 +2235,19 @@ 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)) {
values = Utils.defaults(values, options.where); values = Utils.defaults(values, options.where);
} }
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
// But if we didn't and instance is null, we will throw // Sanity check, ideally we caught this at the defaultFeilds/err.fields check
if (instance === null) throw err; // But if we didn't and instance is null, we will throw
return [instance, false]; if (otherCreated === null) throw err;
});
}); 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];
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err; } catch (err) {
return this.findOne(options).then(result => [result, false]); if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
}); 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,56 +2448,49 @@ class Model { ...@@ -2457,56 +2448,49 @@ class Model {
options.fields = changed; options.fields = changed;
} }
return Promise.resolve().then(() => { if (options.validate) {
if (options.validate) { await instance.validate(options);
return instance.validate(options); }
} // Map field names
}).then(() => { const updatedDataValues = _.pick(instance.dataValues, changed);
// Map field names const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this);
const updatedDataValues = _.pick(instance.dataValues, changed); const updateValues = Utils.mapValueFieldNames(updatedDataValues, options.fields, this);
const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this); const now = Utils.now(this.sequelize.options.dialect);
const updateValues = Utils.mapValueFieldNames(updatedDataValues, options.fields, this);
const now = Utils.now(this.sequelize.options.dialect);
// Attach createdAt
if (createdAtAttr && !updateValues[createdAtAttr]) {
const field = this.rawAttributes[createdAtAttr].field || createdAtAttr;
insertValues[field] = this._getDefaultTimestamp(createdAtAttr) || now;
}
if (updatedAtAttr && !insertValues[updatedAtAttr]) {
const field = this.rawAttributes[updatedAtAttr].field || updatedAtAttr;
insertValues[field] = updateValues[field] = this._getDefaultTimestamp(updatedAtAttr) || now;
}
// Build adds a null value for the primary key, if none was given by the user. // Attach createdAt
// We need to remove that because of some Postgres technicalities. if (createdAtAttr && !updateValues[createdAtAttr]) {
if (!hasPrimary && this.primaryKeyAttribute && !this.rawAttributes[this.primaryKeyAttribute].defaultValue) { const field = this.rawAttributes[createdAtAttr].field || createdAtAttr;
delete insertValues[this.primaryKeyField]; insertValues[field] = this._getDefaultTimestamp(createdAtAttr) || now;
delete updateValues[this.primaryKeyField]; }
} if (updatedAtAttr && !insertValues[updatedAtAttr]) {
const field = this.rawAttributes[updatedAtAttr].field || updatedAtAttr;
insertValues[field] = updateValues[field] = this._getDefaultTimestamp(updatedAtAttr) || now;
}
return Promise.resolve().then(() => { // Build adds a null value for the primary key, if none was given by the user.
if (options.hooks) { // We need to remove that because of some Postgres technicalities.
return this.runHooks('beforeUpsert', values, options); if (!hasPrimary && this.primaryKeyAttribute && !this.rawAttributes[this.primaryKeyAttribute].defaultValue) {
} delete insertValues[this.primaryKeyField];
}) delete updateValues[this.primaryKeyField];
.then(() => { }
return this.QueryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), this, options);
})
.then(([created, primaryKey]) => {
if (options.returning === true && primaryKey) {
return this.findByPk(primaryKey, options).then(record => [record, created]);
}
return created; if (options.hooks) {
}) await this.runHooks('beforeUpsert', values, options);
.then(result => { }
if (options.hooks) { const [created, primaryKey] = await this.QueryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), this, options);
return Promise.resolve(this.runHooks('afterUpsert', result, options)).then(() => result); let result;
} if (options.returning === true && primaryKey) {
return result; const record = await this.findByPk(primaryKey, options);
}); result = [record, created];
}); } else {
result = created;
}
if (options.hooks) {
await this.runHooks('afterUpsert', result, options);
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,52 +2574,47 @@ class Model { ...@@ -2590,52 +2574,47 @@ 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) { await model.runHooks('beforeBulkCreate', instances, options);
return model.runHooks('beforeBulkCreate', instances, options); }
} // Validate
}).then(() => { if (options.validate) {
// Validate const errors = new Promise.AggregateError();
if (options.validate) { const validateOptions = _.clone(options);
const errors = new Promise.AggregateError(); validateOptions.hooks = options.individualHooks;
const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks; await Promise.all(instances.map(async instance => {
try {
return Promise.all(instances.map(instance => await instance.validate(validateOptions);
instance.validate(validateOptions).catch(err => { } catch (err) {
errors.push(new sequelizeErrors.BulkRecordError(err, instance)); 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.all(instances.map(instance => {
const individualOptions = _.clone(options);
delete individualOptions.fields;
delete individualOptions.individualHooks;
delete individualOptions.ignoreDuplicates;
individualOptions.validate = false;
individualOptions.hooks = true;
return instance.save(individualOptions); delete options.skip;
})); if (errors.length) {
throw errors;
} }
}
return Promise.resolve().then(() => { if (options.individualHooks) {
if (!options.include || !options.include.length) return; await Promise.all(instances.map(async instance => {
const individualOptions = _.clone(options);
// Nested creation for BelongsTo relations delete individualOptions.fields;
return Promise.all(options.include.filter(include => include.association instanceof BelongsTo).map(include => { delete individualOptions.individualHooks;
delete individualOptions.ignoreDuplicates;
individualOptions.validate = false;
individualOptions.hooks = true;
await instance.save(individualOptions);
}));
} else {
if (options.include && options.include.length) {
await Promise.all(options.include.filter(include => include.association instanceof BelongsTo).map(async include => {
const associationInstances = []; const associationInstances = [];
const associationInstanceIndexToInstanceMap = []; const associationInstanceIndexToInstanceMap = [];
...@@ -2658,96 +2637,92 @@ class Model { ...@@ -2658,96 +2637,92 @@ 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
// 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)); // Create all in one query
for (const key of model._virtualAttributes) { // Recreate records from instances to represent any changes made in hooks or validation
delete out[key]; records = instances.map(instance => {
} const values = instance.dataValues;
return out;
});
// Map attributes to fields for serial identification // set createdAt/updatedAt attributes
const fieldMappedAttributes = {}; if (createdAtAttr && !values[createdAtAttr]) {
for (const attr in model.tableAttributes) { values[createdAtAttr] = now;
fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr]; if (!options.fields.includes(createdAtAttr)) {
options.fields.push(createdAtAttr);
}
} }
if (updatedAtAttr && !values[updatedAtAttr]) {
// Map updateOnDuplicate attributes to fields values[updatedAtAttr] = now;
if (options.updateOnDuplicate) { if (!options.fields.includes(updatedAtAttr)) {
options.updateOnDuplicate = options.updateOnDuplicate.map(attr => model.rawAttributes[attr].field || attr); options.fields.push(updatedAtAttr);
// Get primary keys for postgres to enable updateOnDuplicate
options.upsertKeys = _.chain(model.primaryKeys).values().map('field').value();
if (Object.keys(model.uniqueKeys).length > 0) {
options.upsertKeys = _.chain(model.uniqueKeys).values().filter(c => c.fields.length >= 1).map(c => c.fields).reduce(c => c[0]).value();
} }
} }
// Map returning attributes to fields const out = Object.assign({}, Utils.mapValueFieldNames(values, options.fields, model));
if (options.returning && Array.isArray(options.returning)) { for (const key of model._virtualAttributes) {
options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr)); delete out[key];
} }
return out;
});
return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => { // Map attributes to fields for serial identification
if (Array.isArray(results)) { const fieldMappedAttributes = {};
results.forEach((result, i) => { for (const attr in model.tableAttributes) {
const instance = instances[i]; fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr];
}
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); // 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('field').value();
if (Object.keys(model.uniqueKeys).length > 0) {
options.upsertKeys = _.chain(model.uniqueKeys).values().filter(c => c.fields.length >= 1).map(c => c.fields).reduce(c => c[0]).value();
}
}
instance.dataValues[attr && attr.fieldName || key] = record; // Map returning attributes to fields
} if (options.returning && Array.isArray(options.returning)) {
} options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr));
}); }
const results = await model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes);
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 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,73 +2753,74 @@ class Model { ...@@ -2778,73 +2753,74 @@ 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 = {};
values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true }); values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = associationInstance.get(associationInstance.constructor.primaryKeyAttribute, { raw: true }); values[include.association.otherKey] = associationInstance.get(associationInstance.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(values, include.association.through.scope);
if (associationInstance[include.association.through.model.name]) { if (associationInstance[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 ||
attr === include.association.foreignKey || attr === include.association.foreignKey ||
attr === include.association.otherKey || attr === include.association.otherKey ||
typeof associationInstance[include.association.through.model.name][attr] === undefined) { typeof associationInstance[include.association.through.model.name][attr] === undefined) {
continue; continue;
}
values[attr] = associationInstance[include.association.through.model.name][attr];
} }
values[attr] = associationInstance[include.association.through.model.name][attr];
} }
valueSets.push(values);
} }
const throughOptions = _(Utils.cloneDeep(include)) valueSets.push(values);
.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); 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);
await recursiveBulkCreate(throughInstances, throughOptions);
} }
instance.isNewRecord = false; }));
}); }
// Run after hook // map fields back to attributes
if (options.hooks) { instances.forEach(instance => {
return model.runHooks('afterBulkCreate', instances, options); 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);
} }
}).then(() => instances); instance.isNewRecord = false;
});
// Run after hook
if (options.hooks) {
await model.runHooks('afterBulkCreate', instances, options);
}
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,52 +2889,48 @@ class Model { ...@@ -2913,52 +2889,48 @@ class Model {
Utils.mapOptionFieldNames(options, this); Utils.mapOptionFieldNames(options, this);
options.model = this; options.model = this;
// Run before hook
if (options.hooks) {
await this.runHooks('beforeBulkDestroy', options);
}
let instances; let instances;
// Get daos and run beforeDestroy hook on each record individually
if (options.individualHooks) {
instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark });
return Promise.resolve().then(() => { await Promise.all(instances.map(instance => this.runHooks('beforeDestroy', instance, options)));
// Run before hook }
if (options.hooks) { let result;
return this.runHooks('beforeBulkDestroy', options); // Run delete query (or update if paranoid)
} if (this._timestampAttributes.deletedAt && !options.force) {
}).then(() => { // Set query type appropriately when running soft delete
// Get daos and run beforeDestroy hook on each record individually options.type = QueryTypes.BULKUPDATE;
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)))) const attrValueHash = {};
.then(_instances => { const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt];
instances = _instances; const field = this.rawAttributes[this._timestampAttributes.deletedAt].field;
}); const where = {
} [field]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null
}).then(() => { };
// Run delete query (or update if paranoid)
if (this._timestampAttributes.deletedAt && !options.force) {
// Set query type appropriately when running soft delete attrValueHash[field] = Utils.now(this.sequelize.options.dialect);
options.type = QueryTypes.BULKUPDATE; result = await this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes);
} else {
const attrValueHash = {}; result = await this.QueryInterface.bulkDelete(this.getTableName(options), options.where, options, this);
const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt]; }
const field = this.rawAttributes[this._timestampAttributes.deletedAt].field; // Run afterDestroy hook on each record individually
const where = { if (options.individualHooks) {
[field]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null await Promise.all(
}; instances.map(instance => this.runHooks('afterDestroy', instance, options))
);
}
attrValueHash[field] = Utils.now(this.sequelize.options.dialect); // Run after hook
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes); if (options.hooks) {
} await this.runHooks('afterBulkDestroy', options);
return this.QueryInterface.bulkDelete(this.getTableName(options), options.where, options, this); }
}).then(result => { return result;
// Run afterDestroy hook on each record individually
if (options.individualHooks) {
return Promise.resolve(Promise.all(instances.map(instance => this.runHooks('afterDestroy', instance, options)))).then(() => result);
}
return result;
}).then(result => {
// Run after hook
if (options.hooks) {
return Promise.resolve(this.runHooks('afterBulkDestroy', options)).then(() => 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,46 +2958,40 @@ class Model { ...@@ -2986,46 +2958,40 @@ 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) { await this.runHooks('beforeBulkRestore', options);
return 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)));
}); }
} // Run undelete query
}).then(() => { const attrValueHash = {};
// Run undelete query const deletedAtCol = this._timestampAttributes.deletedAt;
const attrValueHash = {}; const deletedAtAttribute = this.rawAttributes[deletedAtCol];
const deletedAtCol = this._timestampAttributes.deletedAt; const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
const deletedAtAttribute = this.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue;
options.omitNull = false;
attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue; const result = await this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes);
options.omitNull = false; // Run afterDestroy hook on each record individually
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes); if (options.individualHooks) {
}).then(result => { await Promise.all(
// Run afterDestroy hook on each record individually instances.map(instance => this.runHooks('afterRestore', instance, options))
if (options.individualHooks) { );
return Promise.resolve(Promise.all(instances.map(instance => this.runHooks('afterRestore', instance, options)))).then(() => result); }
} // Run after hook
return result; if (options.hooks) {
}).then(result => { await this.runHooks('afterBulkRestore', options);
// Run after hook }
if (options.hooks) { return result;
return Promise.resolve(this.runHooks('afterBulkRestore', options)).then(() => 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,166 +3058,136 @@ class Model { ...@@ -3092,166 +3058,136 @@ class Model {
options.model = this; options.model = this;
let instances;
let valuesUse; let valuesUse;
// Validate
if (options.validate) {
const build = this.build(values);
build.set(this._timestampAttributes.updatedAt, values[this._timestampAttributes.updatedAt], { raw: true });
return Promise.resolve().then(() => { if (options.sideEffects) {
// Validate values = Object.assign(values, _.pick(build.get(), build.changed()));
if (options.validate) { options.fields = _.union(options.fields, Object.keys(values));
const build = this.build(values);
build.set(this._timestampAttributes.updatedAt, values[this._timestampAttributes.updatedAt], { raw: true });
if (options.sideEffects) {
values = Object.assign(values, _.pick(build.get(), build.changed()));
options.fields = _.union(options.fields, Object.keys(values));
}
// We want to skip validations for all other fields
options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values));
return build.validate(options).then(attributes => {
options.skip = undefined;
if (attributes && attributes.dataValues) {
values = _.pick(attributes.dataValues, Object.keys(values));
}
});
} }
return null;
}).then(() => { // We want to skip validations for all other fields
// Run before hook options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values));
if (options.hooks) { const attributes = await build.validate(options);
options.attributes = values; options.skip = undefined;
return this.runHooks('beforeBulkUpdate', options).then(() => { if (attributes && attributes.dataValues) {
values = options.attributes; values = _.pick(attributes.dataValues, Object.keys(values));
delete options.attributes;
});
} }
return null; }
}).then(() => { // Run before hook
valuesUse = values; if (options.hooks) {
options.attributes = values;
await this.runHooks('beforeBulkUpdate', options);
values = options.attributes;
delete options.attributes;
}
// Get instances and run beforeUpdate hook on each record individually valuesUse = values;
if (options.individualHooks) {
return this.findAll({ // Get instances and run beforeUpdate hook on each record individually
where: options.where, let instances;
transaction: options.transaction, let updateDoneRowByRow = false;
logging: options.logging, if (options.individualHooks) {
benchmark: options.benchmark, instances = await this.findAll({
paranoid: options.paranoid where: options.where,
}).then(_instances => { transaction: options.transaction,
instances = _instances; logging: options.logging,
if (!instances.length) { benchmark: options.benchmark,
return []; paranoid: options.paranoid
} });
// Run beforeUpdate hooks on each record and check whether beforeUpdate hook changes values uniformly if (instances.length) {
// i.e. whether they change values for each record in the same way // Run beforeUpdate hooks on each record and check whether beforeUpdate hook changes values uniformly
let changedValues; // i.e. whether they change values for each record in the same way
let different = false; let changedValues;
let different = false;
instances = await Promise.all(instances.map(async instance => {
// Record updates in instances dataValues
Object.assign(instance.dataValues, values);
// Set the changed fields on the instance
_.forIn(valuesUse, (newValue, attr) => {
if (newValue !== instance._previousDataValues[attr]) {
instance.setDataValue(attr, newValue);
}
});
return Promise.all(instances.map(instance => { // Run beforeUpdate hook
// Record updates in instances dataValues await this.runHooks('beforeUpdate', instance, options);
Object.assign(instance.dataValues, values); if (!different) {
// Set the changed fields on the instance const thisChangedValues = {};
_.forIn(valuesUse, (newValue, attr) => { _.forIn(instance.dataValues, (newValue, attr) => {
if (newValue !== instance._previousDataValues[attr]) { if (newValue !== instance._previousDataValues[attr]) {
instance.setDataValue(attr, newValue); thisChangedValues[attr] = newValue;
} }
}); });
// Run beforeUpdate hook if (!changedValues) {
return this.runHooks('beforeUpdate', instance, options).then(() => { changedValues = thisChangedValues;
if (!different) { } else {
const thisChangedValues = {}; different = !_.isEqual(changedValues, thisChangedValues);
_.forIn(instance.dataValues, (newValue, attr) => { }
if (newValue !== instance._previousDataValues[attr]) { }
thisChangedValues[attr] = newValue;
}
});
if (!changedValues) { return instance;
changedValues = thisChangedValues; }));
} else {
different = !_.isEqual(changedValues, thisChangedValues);
}
}
return instance; if (!different) {
}); const keys = Object.keys(changedValues);
})).then(_instances => { // Hooks do not change values or change them uniformly
instances = _instances; if (keys.length) {
// Hooks change values - record changes in valuesUse so they are executed
if (!different) { valuesUse = changedValues;
const keys = Object.keys(changedValues); options.fields = _.union(options.fields, keys);
// Hooks do not change values or change them uniformly }
if (keys.length) { } else {
// Hooks change values - record changes in valuesUse so they are executed instances = await Promise.all(instances.map(async instance => {
valuesUse = changedValues; const individualOptions = _.clone(options);
options.fields = _.union(options.fields, keys); delete individualOptions.individualHooks;
} individualOptions.hooks = false;
return; individualOptions.validate = false;
}
// 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);
delete individualOptions.individualHooks;
individualOptions.hooks = false;
individualOptions.validate = false;
return instance.save(individualOptions);
})).then(_instances => {
instances = _instances;
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 return instance.save(individualOptions);
if ( }));
_.isEmpty(valuesUse) updateDoneRowByRow = true;
|| 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) {
return Promise.resolve(Promise.all(instances.map(instance => {
return Promise.resolve(this.runHooks('afterUpdate', instance, options)).then(() => result);
})).then(() => {
result[1] = instances;
})).then(() => result);
}
return result;
}).then(result => {
// Run after hook
if (options.hooks) {
options.attributes = values;
return Promise.resolve(this.runHooks('afterBulkUpdate', options).then(() => {
delete options.attributes;
})).then(() => result);
} }
return result; }
});
if (options.individualHooks) {
await Promise.all(instances.map(instance => this.runHooks('afterUpdate', instance, options)));
result[1] = instances;
}
// Run after hook
if (options.hooks) {
options.attributes = values;
await this.runHooks('afterBulkUpdate', options);
delete options.attributes;
}
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,59 +3872,49 @@ class Model { ...@@ -3938,59 +3872,49 @@ 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) { await this.validate(options);
return this.validate(options); }
} // Run before hook
}).then(() => { if (options.hooks) {
// Run before hook const beforeHookValues = _.pick(this.dataValues, options.fields);
if (options.hooks) { let ignoreChanged = _.difference(this.changed(), options.fields); // In case of update where it's only supposed to update the passed values and the hook values
const beforeHookValues = _.pick(this.dataValues, options.fields); let hookChanged;
let ignoreChanged = _.difference(this.changed(), options.fields); // In case of update where it's only supposed to update the passed values and the hook values let afterHookValues;
let hookChanged;
let afterHookValues;
if (updatedAtAttr && options.fields.includes(updatedAtAttr)) { if (updatedAtAttr && options.fields.includes(updatedAtAttr)) {
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));
hookChanged = []; hookChanged = [];
for (const key of Object.keys(afterHookValues)) { for (const key of Object.keys(afterHookValues)) {
if (afterHookValues[key] !== beforeHookValues[key]) { if (afterHookValues[key] !== beforeHookValues[key]) {
hookChanged.push(key); hookChanged.push(key);
} }
} }
options.fields = _.uniq(options.fields.concat(hookChanged)); options.fields = _.uniq(options.fields.concat(hookChanged));
} }
if (hookChanged) { if (hookChanged) {
if (options.validate) { if (options.validate) {
// 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) return this; if (options.fields.length && this.isNewRecord && this._options.include && this._options.include.length) {
if (!this.isNewRecord) return this; await Promise.all(this._options.include.filter(include => include.association instanceof BelongsTo).map(async include => {
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,129 +3924,120 @@ class Model { ...@@ -4000,129 +3924,120 @@ 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;
const versionFieldName = _.get(this.constructor.rawAttributes[versionAttr], 'field') || versionAttr; const versionFieldName = _.get(this.constructor.rawAttributes[versionAttr], 'field') || versionAttr;
let values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.constructor); let values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.constructor);
let query = null; let query = null;
let args = []; let args = [];
let where; let where;
if (this.isNewRecord) { if (this.isNewRecord) {
query = 'insert'; query = 'insert';
args = [this, this.constructor.getTableName(options), values, options]; args = [this, this.constructor.getTableName(options), values, options];
} else {
where = this.where(true);
if (versionAttr) {
values[versionFieldName] = parseInt(values[versionFieldName], 10) + 1;
}
query = 'update';
args = [this, this.constructor.getTableName(options), values, where, options];
}
const [result, rowsUpdated] = await this.constructor.QueryInterface[query](...args);
if (versionAttr) {
// Check to see that a row was updated, otherwise it's an optimistic locking error.
if (rowsUpdated < 1) {
throw new sequelizeErrors.OptimisticLockError({
modelName: this.constructor.name,
values,
where
});
} else { } else {
where = this.where(true); result.dataValues[versionAttr] = values[versionFieldName];
if (versionAttr) {
values[versionFieldName] = parseInt(values[versionFieldName], 10) + 1;
}
query = 'update';
args = [this, this.constructor.getTableName(options), values, where, options];
} }
}
return this.constructor.QueryInterface[query](...args) // Transfer database generated values (defaults, autoincrement, etc)
.then(([result, rowsUpdated])=> { for (const attr of Object.keys(this.constructor.rawAttributes)) {
if (versionAttr) { if (this.constructor.rawAttributes[attr].field &&
// Check to see that a row was updated, otherwise it's an optimistic locking error. values[this.constructor.rawAttributes[attr].field] !== undefined &&
if (rowsUpdated < 1) { this.constructor.rawAttributes[attr].field !== attr
throw new sequelizeErrors.OptimisticLockError({ ) {
modelName: this.constructor.name, values[attr] = values[this.constructor.rawAttributes[attr].field];
values, delete values[this.constructor.rawAttributes[attr].field];
where }
}); }
} else { values = Object.assign(values, result.dataValues);
result.dataValues[versionAttr] = values[versionFieldName];
}
}
// Transfer database generated values (defaults, autoincrement, etc)
for (const attr of Object.keys(this.constructor.rawAttributes)) {
if (this.constructor.rawAttributes[attr].field &&
values[this.constructor.rawAttributes[attr].field] !== undefined &&
this.constructor.rawAttributes[attr].field !== attr
) {
values[attr] = values[this.constructor.rawAttributes[attr].field];
delete values[this.constructor.rawAttributes[attr].field];
}
}
values = Object.assign(values, result.dataValues);
result.dataValues = Object.assign(result.dataValues, values);
return result;
})
.then(result => {
if (!wasNewRecord) return Promise.resolve(this).then(() => result);
if (!this._options.include || !this._options.include.length) return Promise.resolve(this).then(() => result);
// Nested creation for HasOne/HasMany/BelongsToMany relations result.dataValues = Object.assign(result.dataValues, values);
return Promise.resolve(Promise.all(this._options.include.filter(include => !(include.association instanceof BelongsTo || if (wasNewRecord && this._options.include && this._options.include.length) {
include.parent && include.parent.association instanceof BelongsToMany)).map(include => { await Promise.all(
let instances = this.get(include.as); this._options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)).map(async include => {
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'])
.defaults({ .defaults({
transaction: options.transaction, transaction: options.transaction,
logging: options.logging, logging: options.logging,
parentRecord: this parentRecord: this
}).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 ||
attr === include.association.foreignKey || attr === include.association.foreignKey ||
attr === include.association.otherKey || attr === include.association.otherKey ||
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); }
})).then(() => result);
} }
await include.association.throughModel.create(values0, includeOptions);
} 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
if (options.hooks) {
return Promise.resolve(this.constructor.runHooks(`after${hook}`, result, options)).then(() => result);
}
return result;
}) })
.then(result => { );
for (const field of options.fields) { }
result._previousDataValues[field] = result.dataValues[field]; // Run after hook
this.changed(field, false); if (options.hooks) {
} await this.constructor.runHooks(`after${hook}`, result, options);
this.isNewRecord = false; }
return result; for (const field of options.fields) {
}); result._previousDataValues[field] = result.dataValues[field];
}); this.changed(field, false);
}
this.isNewRecord = false;
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)' );
); }
} // update the internal options of the instance
return reload; this._options = reloaded._options;
}) // re-set instance values
.then(reload => { this.set(reloaded.dataValues, {
// update the internal options of the instance raw: true,
this._options = reload._options; reset: true && !options.attributes
// re-set instance values });
this.set(reload.dataValues, {
raw: true, return this;
reset: true && !options.attributes
});
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,43 +4143,41 @@ class Model { ...@@ -4232,43 +4143,41 @@ 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) { await this.constructor.runHooks('beforeDestroy', this, options);
return this.constructor.runHooks('beforeDestroy', this, options); }
} const where = this.where(true);
}).then(() => {
const where = this.where(true);
if (this.constructor._timestampAttributes.deletedAt && options.force === false) {
const attributeName = this.constructor._timestampAttributes.deletedAt;
const attribute = this.constructor.rawAttributes[attributeName];
const defaultValue = Object.prototype.hasOwnProperty.call(attribute, 'defaultValue')
? attribute.defaultValue
: null;
const currentValue = this.getDataValue(attributeName);
const undefinedOrNull = currentValue == null && defaultValue == null;
if (undefinedOrNull || _.isEqual(currentValue, defaultValue)) {
// only update timestamp if it wasn't already set
this.setDataValue(attributeName, new Date());
}
return this.save(_.defaults({ hooks: false }, options)); let result;
if (this.constructor._timestampAttributes.deletedAt && options.force === false) {
const attributeName = this.constructor._timestampAttributes.deletedAt;
const attribute = this.constructor.rawAttributes[attributeName];
const defaultValue = Object.prototype.hasOwnProperty.call(attribute, 'defaultValue')
? attribute.defaultValue
: null;
const currentValue = this.getDataValue(attributeName);
const undefinedOrNull = currentValue == null && defaultValue == null;
if (undefinedOrNull || _.isEqual(currentValue, defaultValue)) {
// only update timestamp if it wasn't already set
this.setDataValue(attributeName, new Date());
} }
return this.constructor.QueryInterface.delete(this, this.constructor.getTableName(options), where, Object.assign({ type: QueryTypes.DELETE, limit: null }, options));
}).then(result => { result = await this.save(_.defaults({ hooks: false }, options));
// Run after hook } else {
if (options.hooks) { result = await this.constructor.QueryInterface.delete(this, this.constructor.getTableName(options), where, Object.assign({ type: QueryTypes.DELETE, limit: null }, options));
return Promise.resolve(this.constructor.runHooks('afterDestroy', this, options)).then(() => result); }
} // Run after hook
return result; if (options.hooks) {
}); await this.constructor.runHooks('afterDestroy', this, options);
}
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) { await this.constructor.runHooks('beforeRestore', this, options);
return this.constructor.runHooks('beforeRestore', this, options); }
} const deletedAtCol = this.constructor._timestampAttributes.deletedAt;
}).then(() => { const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol];
const deletedAtCol = this.constructor._timestampAttributes.deletedAt; const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null; this.setDataValue(deletedAtCol, deletedAtDefaultValue);
const result = await this.save(Object.assign({}, options, { hooks: false, omitNull: false }));
this.setDataValue(deletedAtCol, deletedAtDefaultValue); // Run after hook
return this.save(Object.assign({}, options, { hooks: false, omitNull: false })); if (options.hooks) {
}).then(result => { await this.constructor.runHooks('afterRestore', this, options);
// Run after hook
if (options.hooks) {
return Promise.resolve(this.constructor.runHooks('afterRestore', this, options)).then(() => result);
}
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!