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

Commit 81ce8ee3 by Andy Edwards Committed by GitHub

refactor(model): asyncify methods (#12122)

1 parent bccb447b
......@@ -1380,12 +1380,12 @@ class Model {
*
* @returns {Promise}
*/
static drop(options) {
return this.QueryInterface.dropTable(this.getTableName(options), options);
static async drop(options) {
return await this.QueryInterface.dropTable(this.getTableName(options), options);
}
static dropSchema(schema) {
return this.QueryInterface.dropSchema(schema);
static async dropSchema(schema) {
return await this.QueryInterface.dropSchema(schema);
}
/**
......@@ -1675,7 +1675,7 @@ class Model {
*
* @returns {Promise<Array<Model>>}
*/
static findAll(options) {
static async findAll(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');
}
......@@ -1700,78 +1700,70 @@ class Model {
? options.rejectOnEmpty
: this.options.rejectOnEmpty;
return Promise.resolve().then(() => {
this._injectScope(options);
this._injectScope(options);
if (options.hooks) {
return this.runHooks('beforeFind', options);
}
}).then(() => {
this._conformIncludes(options, this);
this._expandAttributes(options);
this._expandIncludeAll(options);
if (options.hooks) {
await this.runHooks('beforeFind', options);
}
this._conformIncludes(options, this);
this._expandAttributes(options);
this._expandIncludeAll(options);
if (options.hooks) {
return this.runHooks('beforeFindAfterExpandIncludeAll', options);
}
}).then(() => {
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
if (options.hooks) {
await this.runHooks('beforeFindAfterExpandIncludeAll', options);
}
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
if (options.include) {
options.hasJoin = true;
if (options.include) {
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 (
options.attributes
&& !options.raw
&& this.primaryKeyAttribute
&& !options.attributes.includes(this.primaryKeyAttribute)
&& (!options.group || !options.hasSingleAssociation || options.hasMultiAssociation)
) {
options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
}
// If we're not raw, we have to make sure we include the primary key for de-duplication
if (
options.attributes
&& !options.raw
&& this.primaryKeyAttribute
&& !options.attributes.includes(this.primaryKeyAttribute)
&& (!options.group || !options.hasSingleAssociation || options.hasMultiAssociation)
) {
options.attributes = [this.primaryKeyAttribute].concat(options.attributes);
}
}
if (!options.attributes) {
options.attributes = Object.keys(this.rawAttributes);
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
}
if (!options.attributes) {
options.attributes = Object.keys(this.rawAttributes);
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
}
// whereCollection is used for non-primary key updates
this.options.whereCollection = options.where || null;
// whereCollection is used for non-primary key updates
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) {
return this.runHooks('beforeFindAfterOptions', options);
}
}).then(() => {
const selectOptions = Object.assign({}, options, { tableNames: Object.keys(tableNames) });
return this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions);
}).then(results => {
if (options.hooks) {
return Promise.resolve(this.runHooks('afterFind', results, options)).then(() => results);
}
return results;
}).then(results => {
if (options.hooks) {
await this.runHooks('beforeFindAfterOptions', options);
}
const selectOptions = Object.assign({}, options, { tableNames: Object.keys(tableNames) });
const results = await this.QueryInterface.select(this, this.getTableName(selectOptions), selectOptions);
if (options.hooks) {
await this.runHooks('afterFind', results, options);
}
//rejectOnEmpty mode
if (_.isEmpty(results) && options.rejectOnEmpty) {
if (typeof options.rejectOnEmpty === 'function') {
throw new options.rejectOnEmpty();
}
if (typeof options.rejectOnEmpty === 'object') {
throw options.rejectOnEmpty;
}
throw new sequelizeErrors.EmptyResultError();
//rejectOnEmpty mode
if (_.isEmpty(results) && options.rejectOnEmpty) {
if (typeof options.rejectOnEmpty === 'function') {
throw new options.rejectOnEmpty();
}
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) {
......@@ -1804,17 +1796,17 @@ class Model {
return attributes;
}
static _findSeparate(results, options) {
if (!options.include || options.raw || !results) return Promise.resolve(results);
static async _findSeparate(results, options) {
if (!options.include || options.raw || !results) return results;
const original = results;
if (options.plain) results = [results];
if (!results.length) return original;
return Promise.all(options.include.map(include => {
await Promise.all(options.include.map(async include => {
if (!include.separate) {
return Model._findSeparate(
return await Model._findSeparate(
results.reduce((memo, result) => {
let associations = result.get(include.association.as);
......@@ -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(include, ['parent', 'association', 'as', 'originalAttributes'])
)).then(map => {
for (const result of results) {
result.set(
include.association.as,
map[result.get(include.association.sourceKey)],
{ raw: true }
);
}
});
})).then(() => original);
));
for (const result of results) {
result.set(
include.association.as,
map[result.get(include.association.sourceKey)],
{ raw: true }
);
}
}));
return original;
}
/**
......@@ -1866,10 +1860,10 @@ class Model {
*
* @returns {Promise<Model>}
*/
static findByPk(param, options) {
static async findByPk(param, options) {
// return Promise resolved with null if no arguments are passed
if ([null, undefined].includes(param)) {
return Promise.resolve(null);
return null;
}
options = Utils.cloneDeep(options) || {};
......@@ -1883,7 +1877,7 @@ class Model {
}
// Bypass a possible overloaded findOne
return this.findOne(options);
return await this.findOne(options);
}
/**
......@@ -1898,7 +1892,7 @@ class Model {
*
* @returns {Promise<Model|null>}
*/
static findOne(options) {
static async findOne(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');
}
......@@ -1917,7 +1911,7 @@ class Model {
}
// Bypass a possible overloaded findAll.
return this.findAll(_.defaults(options, {
return await this.findAll(_.defaults(options, {
plain: true
}));
}
......@@ -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.
*/
static aggregate(attribute, aggregateFunction, options) {
static async aggregate(attribute, aggregateFunction, options) {
options = Utils.cloneDeep(options);
// We need to preserve attributes here as the `injectScope` call would inject non aggregate columns.
......@@ -1986,12 +1980,11 @@ class Model {
Utils.mapOptionFieldNames(options, this);
options = this._paranoidClause(this, options);
return this.QueryInterface.rawSelect(this.getTableName(options), options, aggregateFunction, this).then( value => {
if (value === null) {
return 0;
}
return value;
});
const value = await this.QueryInterface.rawSelect(this.getTableName(options), options, aggregateFunction, this);
if (value === null) {
return 0;
}
return value;
}
/**
......@@ -2014,34 +2007,31 @@ class Model {
*
* @returns {Promise<number>}
*/
static count(options) {
return Promise.resolve().then(() => {
options = Utils.cloneDeep(options);
options = _.defaults(options, { hooks: true });
options.raw = true;
if (options.hooks) {
return this.runHooks('beforeCount', options);
}
}).then(() => {
let col = options.col || '*';
if (options.include) {
col = `${this.name}.${options.col || this.primaryKeyField}`;
}
if (options.distinct && col === '*') {
col = this.primaryKeyField;
}
options.plain = !options.group;
options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false;
static async count(options) {
options = Utils.cloneDeep(options);
options = _.defaults(options, { hooks: true });
options.raw = true;
if (options.hooks) {
await this.runHooks('beforeCount', options);
}
let col = options.col || '*';
if (options.include) {
col = `${this.name}.${options.col || this.primaryKeyField}`;
}
if (options.distinct && col === '*') {
col = this.primaryKeyField;
}
options.plain = !options.group;
options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false;
// No limit, offset or order for the options max be given to count()
// Set them to null to prevent scopes setting those values
options.limit = null;
options.offset = null;
options.order = null;
// No limit, offset or order for the options max be given to count()
// Set them to null to prevent scopes setting those values
options.limit = null;
options.offset = null;
options.order = null;
return this.aggregate(col, 'count', options);
});
return await this.aggregate(col, 'count', options);
}
/**
......@@ -2080,7 +2070,7 @@ class Model {
*
* @returns {Promise<{count: number, rows: Model[]}>}
*/
static findAndCountAll(options) {
static async findAndCountAll(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');
}
......@@ -2091,14 +2081,15 @@ class Model {
countOptions.attributes = undefined;
}
return Promise.all([
const [count, rows] = await Promise.all([
this.count(countOptions),
this.findAll(options)
])
.then(([count, rows]) => ({
count,
rows: count === 0 ? [] : rows
}));
]);
return {
count,
rows: count === 0 ? [] : rows
};
}
/**
......@@ -2112,8 +2103,8 @@ class Model {
*
* @returns {Promise<*>}
*/
static max(field, options) {
return this.aggregate(field, 'max', options);
static async max(field, options) {
return await this.aggregate(field, 'max', options);
}
/**
......@@ -2127,8 +2118,8 @@ class Model {
*
* @returns {Promise<*>}
*/
static min(field, options) {
return this.aggregate(field, 'min', options);
static async min(field, options) {
return await this.aggregate(field, 'min', options);
}
/**
......@@ -2142,8 +2133,8 @@ class Model {
*
* @returns {Promise<number>}
*/
static sum(field, options) {
return this.aggregate(field, 'sum', options);
static async sum(field, options) {
return await this.aggregate(field, 'sum', options);
}
/**
......@@ -2211,10 +2202,10 @@ class Model {
* @returns {Promise<Model>}
*
*/
static create(values, options) {
static async create(values, options) {
options = Utils.cloneDeep(options || {});
return this.build(values, {
return await this.build(values, {
isNewRecord: true,
attributes: options.fields,
include: options.include,
......@@ -2234,7 +2225,7 @@ class Model {
*
* @returns {Promise<Model,boolean>}
*/
static findOrBuild(options) {
static async findOrBuild(options) {
if (!options || !options.where || arguments.length > 1) {
throw new Error(
'Missing where attribute in the options parameter passed to findOrBuild. ' +
......@@ -2244,20 +2235,19 @@ class Model {
let values;
return this.findOne(options).then(instance => {
if (instance === null) {
values = _.clone(options.defaults) || {};
if (_.isPlainObject(options.where)) {
values = Utils.defaults(values, options.where);
}
let instance = await this.findOne(options);
if (instance === null) {
values = _.clone(options.defaults) || {};
if (_.isPlainObject(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 {
*
* @returns {Promise<Model,boolean>}
*/
static findOrCreate(options) {
static async findOrCreate(options) {
if (!options || !options.where || arguments.length > 1) {
throw new Error(
'Missing where attribute in the options parameter passed to findOrCreate. ' +
......@@ -2308,15 +2298,14 @@ class Model {
let values;
let transaction;
// Create a transaction or a savepoint, depending on whether a transaction was passed in
return this.sequelize.transaction(options).then(t => {
try {
const t = await this.sequelize.transaction(options);
transaction = t;
options.transaction = t;
return this.findOne(Utils.defaults({ transaction }, options));
}).then(instance => {
if (instance !== null) {
return [instance, false];
const found = await this.findOne(Utils.defaults({ transaction }, options));
if (found !== null) {
return [found, false];
}
values = _.clone(options.defaults) || {};
......@@ -2327,14 +2316,15 @@ class Model {
options.exception = true;
options.returning = true;
return this.create(values, options).then(instance => {
if (instance.get(this.primaryKeyAttribute, { raw: true }) === null) {
try {
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
throw new sequelizeErrors.UniqueConstraintError();
}
return [instance, true];
}).catch(err => {
return [created, true];
} catch (err) {
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
const flattenedWhere = Utils.flattenObjectDeep(options.where);
const flattenedWhereKeys = Object.keys(flattenedWhere).map(name => _.last(name.split('.')));
......@@ -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!
return this.findOne(Utils.defaults({
const otherCreated = await this.findOne(Utils.defaults({
transaction: internalTransaction ? null : transaction
}, options)).then(instance => {
// Sanity check, ideally we caught this at the defaultFeilds/err.fields check
// But if we didn't and instance is null, we will throw
if (instance === null) throw err;
return [instance, false];
});
});
}).finally(() => {
}, 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
if (otherCreated === null) throw err;
return [otherCreated, false];
}
} finally {
if (internalTransaction && transaction) {
// If we created a transaction internally (and not just a savepoint), we should clean it up
return transaction.commit();
await transaction.commit();
}
});
}
}
/**
......@@ -2389,7 +2379,7 @@ class Model {
*
* @returns {Promise<Model,boolean>}
*/
static findCreateFind(options) {
static async findCreateFind(options) {
if (!options || !options.where) {
throw new Error(
'Missing where attribute in the options parameter passed to findCreateFind.'
......@@ -2402,16 +2392,17 @@ class Model {
}
return this.findOne(options).then(result => {
if (result) return [result, false];
const found = await this.findOne(options);
if (found) return [found, false];
return this.create(values, options)
.then(result => [result, true])
.catch(err => {
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
return this.findOne(options).then(result => [result, false]);
});
});
try {
const created = await this.create(values, options);
return [created, true];
} catch (err) {
if (!(err instanceof sequelizeErrors.UniqueConstraintError)) throw err;
const foundAgain = await this.findOne(options);
return [foundAgain, false];
}
}
/**
......@@ -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>`.
*/
static upsert(values, options) {
static async upsert(values, options) {
options = Object.assign({
hooks: true,
returning: false,
......@@ -2457,56 +2448,49 @@ class Model {
options.fields = changed;
}
return Promise.resolve().then(() => {
if (options.validate) {
return instance.validate(options);
}
}).then(() => {
// Map field names
const updatedDataValues = _.pick(instance.dataValues, changed);
const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this);
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;
}
if (options.validate) {
await instance.validate(options);
}
// Map field names
const updatedDataValues = _.pick(instance.dataValues, changed);
const insertValues = Utils.mapValueFieldNames(instance.dataValues, Object.keys(instance.rawAttributes), this);
const updateValues = Utils.mapValueFieldNames(updatedDataValues, options.fields, this);
const now = Utils.now(this.sequelize.options.dialect);
// Build adds a null value for the primary key, if none was given by the user.
// We need to remove that because of some Postgres technicalities.
if (!hasPrimary && this.primaryKeyAttribute && !this.rawAttributes[this.primaryKeyAttribute].defaultValue) {
delete insertValues[this.primaryKeyField];
delete updateValues[this.primaryKeyField];
}
// 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;
}
return Promise.resolve().then(() => {
if (options.hooks) {
return this.runHooks('beforeUpsert', values, options);
}
})
.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]);
}
// Build adds a null value for the primary key, if none was given by the user.
// We need to remove that because of some Postgres technicalities.
if (!hasPrimary && this.primaryKeyAttribute && !this.rawAttributes[this.primaryKeyAttribute].defaultValue) {
delete insertValues[this.primaryKeyField];
delete updateValues[this.primaryKeyField];
}
return created;
})
.then(result => {
if (options.hooks) {
return Promise.resolve(this.runHooks('afterUpsert', result, options)).then(() => result);
}
return result;
});
});
if (options.hooks) {
await this.runHooks('beforeUpsert', values, options);
}
const [created, primaryKey] = await this.QueryInterface.upsert(this.getTableName(options), insertValues, updateValues, instance.where(), this, options);
let result;
if (options.returning === true && primaryKey) {
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 {
*
* @returns {Promise<Array<Model>>}
*/
static bulkCreate(records, options = {}) {
static async bulkCreate(records, options = {}) {
if (!records.length) {
return Promise.resolve([]);
return [];
}
const dialect = this.sequelize.options.dialect;
......@@ -2554,7 +2538,7 @@ class Model {
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({
validate: false,
hooks: true,
......@@ -2571,10 +2555,10 @@ class Model {
}
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')) {
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;
......@@ -2590,52 +2574,47 @@ class Model {
options.updateOnDuplicate
);
} 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
if (options.hooks) {
return model.runHooks('beforeBulkCreate', instances, options);
}
}).then(() => {
// Validate
if (options.validate) {
const errors = new Promise.AggregateError();
const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks;
return Promise.all(instances.map(instance =>
instance.validate(validateOptions).catch(err => {
errors.push(new sequelizeErrors.BulkRecordError(err, instance));
}))).then(() => {
delete options.skip;
if (errors.length) {
throw errors;
}
});
}
}).then(() => {
if (options.individualHooks) {
// Create each instance individually
return Promise.all(instances.map(instance => {
const individualOptions = _.clone(options);
delete individualOptions.fields;
delete individualOptions.individualHooks;
delete individualOptions.ignoreDuplicates;
individualOptions.validate = false;
individualOptions.hooks = true;
// Run before hook
if (options.hooks) {
await model.runHooks('beforeBulkCreate', instances, options);
}
// Validate
if (options.validate) {
const errors = new Promise.AggregateError();
const validateOptions = _.clone(options);
validateOptions.hooks = options.individualHooks;
await Promise.all(instances.map(async instance => {
try {
await instance.validate(validateOptions);
} catch (err) {
errors.push(new sequelizeErrors.BulkRecordError(err, instance));
}
}));
return instance.save(individualOptions);
}));
delete options.skip;
if (errors.length) {
throw errors;
}
return Promise.resolve().then(() => {
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 => {
}
if (options.individualHooks) {
await Promise.all(instances.map(async instance => {
const individualOptions = _.clone(options);
delete individualOptions.fields;
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 associationInstanceIndexToInstanceMap = [];
......@@ -2658,96 +2637,92 @@ class Model {
logging: options.logging
}).value();
return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => {
for (const idx in associationInstances) {
const associationInstance = associationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx];
const createdAssociationInstances = await recursiveBulkCreate(associationInstances, includeOptions);
for (const idx in createdAssociationInstances) {
const associationInstance = createdAssociationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx];
instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging });
}
});
}));
}).then(() => {
// Create all in one query
// Recreate records from instances to represent any changes made in hooks or validation
records = instances.map(instance => {
const values = instance.dataValues;
// set createdAt/updatedAt attributes
if (createdAtAttr && !values[createdAtAttr]) {
values[createdAtAttr] = now;
if (!options.fields.includes(createdAtAttr)) {
options.fields.push(createdAtAttr);
}
}
if (updatedAtAttr && !values[updatedAtAttr]) {
values[updatedAtAttr] = now;
if (!options.fields.includes(updatedAtAttr)) {
options.fields.push(updatedAtAttr);
}
instance[include.association.accessors.set](associationInstance, { save: false, logging: options.logging });
}
}));
}
const out = Object.assign({}, Utils.mapValueFieldNames(values, options.fields, model));
for (const key of model._virtualAttributes) {
delete out[key];
}
return out;
});
// 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;
// Map attributes to fields for serial identification
const fieldMappedAttributes = {};
for (const attr in model.tableAttributes) {
fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr];
// set createdAt/updatedAt attributes
if (createdAtAttr && !values[createdAtAttr]) {
values[createdAtAttr] = now;
if (!options.fields.includes(createdAtAttr)) {
options.fields.push(createdAtAttr);
}
}
// 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();
if (updatedAtAttr && !values[updatedAtAttr]) {
values[updatedAtAttr] = now;
if (!options.fields.includes(updatedAtAttr)) {
options.fields.push(updatedAtAttr);
}
}
// 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 out = Object.assign({}, Utils.mapValueFieldNames(values, options.fields, model));
for (const key of model._virtualAttributes) {
delete out[key];
}
return out;
});
return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => {
if (Array.isArray(results)) {
results.forEach((result, i) => {
const instance = instances[i];
for (const key in result) {
if (!instance || key === model.primaryKeyAttribute &&
instance.get(model.primaryKeyAttribute) &&
['mysql', 'mariadb', 'sqlite'].includes(dialect)) {
// The query.js for these DBs is blind, it autoincrements the
// primarykey value, even if it was set manually. Also, it can
// return more results than instances, bug?.
continue;
}
if (Object.prototype.hasOwnProperty.call(result, key)) {
const record = result[key];
// Map attributes to fields for serial identification
const fieldMappedAttributes = {};
for (const attr in model.tableAttributes) {
fieldMappedAttributes[model.rawAttributes[attr].field || attr] = model.rawAttributes[attr];
}
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
return Promise.all(options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)).map(include => {
if (options.include && options.include.length) {
await Promise.all(options.include.filter(include => !(include.association instanceof BelongsTo ||
include.parent && include.parent.association instanceof BelongsToMany)).map(async include => {
const associationInstances = [];
const associationInstanceIndexToInstanceMap = [];
......@@ -2778,73 +2753,74 @@ class Model {
logging: options.logging
}).value();
return recursiveBulkCreate(associationInstances, includeOptions).then(associationInstances => {
if (include.association instanceof BelongsToMany) {
const valueSets = [];
for (const idx in associationInstances) {
const associationInstance = associationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx];
const values = {};
values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = associationInstance.get(associationInstance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association
Object.assign(values, include.association.through.scope);
if (associationInstance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
attr === include.association.foreignKey ||
attr === include.association.otherKey ||
typeof associationInstance[include.association.through.model.name][attr] === undefined) {
continue;
}
values[attr] = associationInstance[include.association.through.model.name][attr];
const createdAssociationInstances = await recursiveBulkCreate(associationInstances, includeOptions);
if (include.association instanceof BelongsToMany) {
const valueSets = [];
for (const idx in createdAssociationInstances) {
const associationInstance = createdAssociationInstances[idx];
const instance = associationInstanceIndexToInstanceMap[idx];
const values = {};
values[include.association.foreignKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = associationInstance.get(associationInstance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association
Object.assign(values, include.association.through.scope);
if (associationInstance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
attr === include.association.foreignKey ||
attr === include.association.otherKey ||
typeof associationInstance[include.association.through.model.name][attr] === undefined) {
continue;
}
values[attr] = associationInstance[include.association.through.model.name][attr];
}
valueSets.push(values);
}
const throughOptions = _(Utils.cloneDeep(include))
.omit(['association', 'attributes'])
.defaults({
transaction: options.transaction,
logging: options.logging
}).value();
throughOptions.model = include.association.throughModel;
const throughInstances = include.association.throughModel.bulkBuild(valueSets, throughOptions);
return recursiveBulkCreate(throughInstances, throughOptions);
}
});
}));
}).then(() => {
// map fields back to attributes
instances.forEach(instance => {
for (const attr in model.rawAttributes) {
if (model.rawAttributes[attr].field &&
instance.dataValues[model.rawAttributes[attr].field] !== undefined &&
model.rawAttributes[attr].field !== attr
) {
instance.dataValues[attr] = instance.dataValues[model.rawAttributes[attr].field];
delete instance.dataValues[model.rawAttributes[attr].field];
valueSets.push(values);
}
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
if (options.hooks) {
return model.runHooks('afterBulkCreate', instances, options);
// 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);
}
}).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 {
* @see
* {@link Model.destroy} for more information
*/
static truncate(options) {
static async truncate(options) {
options = Utils.cloneDeep(options) || {};
options.truncate = true;
return this.destroy(options);
return await this.destroy(options);
}
/**
......@@ -2887,7 +2863,7 @@ class Model {
*
* @returns {Promise<number>} The number of destroyed rows
*/
static destroy(options) {
static async destroy(options) {
options = Utils.cloneDeep(options);
this._injectScope(options);
......@@ -2913,52 +2889,48 @@ class Model {
Utils.mapOptionFieldNames(options, this);
options.model = this;
// Run before hook
if (options.hooks) {
await this.runHooks('beforeBulkDestroy', options);
}
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(() => {
// Run before hook
if (options.hooks) {
return this.runHooks('beforeBulkDestroy', options);
}
}).then(() => {
// Get daos and run beforeDestroy hook on each record individually
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))))
.then(_instances => {
instances = _instances;
});
}
}).then(() => {
// Run delete query (or update if paranoid)
if (this._timestampAttributes.deletedAt && !options.force) {
// Set query type appropriately when running soft delete
options.type = QueryTypes.BULKUPDATE;
const attrValueHash = {};
const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt];
const field = this.rawAttributes[this._timestampAttributes.deletedAt].field;
const where = {
[field]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null
};
attrValueHash[field] = Utils.now(this.sequelize.options.dialect);
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, Object.assign(where, options.where), options, this.rawAttributes);
}
return this.QueryInterface.bulkDelete(this.getTableName(options), options.where, options, this);
}).then(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;
});
await Promise.all(instances.map(instance => this.runHooks('beforeDestroy', instance, options)));
}
let result;
// Run delete query (or update if paranoid)
if (this._timestampAttributes.deletedAt && !options.force) {
// Set query type appropriately when running soft delete
options.type = QueryTypes.BULKUPDATE;
const attrValueHash = {};
const deletedAtAttribute = this.rawAttributes[this._timestampAttributes.deletedAt];
const field = this.rawAttributes[this._timestampAttributes.deletedAt].field;
const where = {
[field]: Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null
};
attrValueHash[field] = Utils.now(this.sequelize.options.dialect);
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);
}
// Run afterDestroy hook on each record individually
if (options.individualHooks) {
await Promise.all(
instances.map(instance => this.runHooks('afterDestroy', instance, options))
);
}
// Run after hook
if (options.hooks) {
await this.runHooks('afterBulkDestroy', options);
}
return result;
}
/**
......@@ -2975,7 +2947,7 @@ class Model {
*
* @returns {Promise}
*/
static restore(options) {
static async restore(options) {
if (!this._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Object.assign({
......@@ -2986,46 +2958,40 @@ class Model {
options.type = QueryTypes.RAW;
options.model = this;
let instances;
Utils.mapOptionFieldNames(options, this);
return Promise.resolve().then(() => {
// Run before hook
if (options.hooks) {
return this.runHooks('beforeBulkRestore', options);
}
}).then(() => {
// Get daos and run beforeRestore hook on each record individually
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))))
.then(_instances => {
instances = _instances;
});
}
}).then(() => {
// Run undelete query
const attrValueHash = {};
const deletedAtCol = this._timestampAttributes.deletedAt;
const deletedAtAttribute = this.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue;
options.omitNull = false;
return this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes);
}).then(result => {
// Run afterDestroy hook on each record individually
if (options.individualHooks) {
return Promise.resolve(Promise.all(instances.map(instance => this.runHooks('afterRestore', instance, options)))).then(() => result);
}
return result;
}).then(result => {
// Run after hook
if (options.hooks) {
return Promise.resolve(this.runHooks('afterBulkRestore', options)).then(() => result);
}
return result;
});
// Run before hook
if (options.hooks) {
await this.runHooks('beforeBulkRestore', options);
}
let instances;
// Get daos and run beforeRestore hook on each record individually
if (options.individualHooks) {
instances = await this.findAll({ where: options.where, transaction: options.transaction, logging: options.logging, benchmark: options.benchmark, paranoid: false });
await Promise.all(instances.map(instance => this.runHooks('beforeRestore', instance, options)));
}
// Run undelete query
const attrValueHash = {};
const deletedAtCol = this._timestampAttributes.deletedAt;
const deletedAtAttribute = this.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
attrValueHash[deletedAtAttribute.field || deletedAtCol] = deletedAtDefaultValue;
options.omitNull = false;
const result = await this.QueryInterface.bulkUpdate(this.getTableName(options), attrValueHash, options.where, options, this.rawAttributes);
// Run afterDestroy hook on each record individually
if (options.individualHooks) {
await Promise.all(
instances.map(instance => this.runHooks('afterRestore', instance, options))
);
}
// Run after hook
if (options.hooks) {
await this.runHooks('afterBulkRestore', options);
}
return result;
}
/**
......@@ -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).
*
*/
static update(values, options) {
static async update(values, options) {
options = Utils.cloneDeep(options);
this._injectScope(options);
......@@ -3092,166 +3058,136 @@ class Model {
options.model = this;
let instances;
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(() => {
// Validate
if (options.validate) {
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));
}
});
if (options.sideEffects) {
values = Object.assign(values, _.pick(build.get(), build.changed()));
options.fields = _.union(options.fields, Object.keys(values));
}
return null;
}).then(() => {
// Run before hook
if (options.hooks) {
options.attributes = values;
return this.runHooks('beforeBulkUpdate', options).then(() => {
values = options.attributes;
delete options.attributes;
});
// We want to skip validations for all other fields
options.skip = _.difference(Object.keys(this.rawAttributes), Object.keys(values));
const attributes = await build.validate(options);
options.skip = undefined;
if (attributes && attributes.dataValues) {
values = _.pick(attributes.dataValues, Object.keys(values));
}
return null;
}).then(() => {
valuesUse = values;
}
// Run before hook
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
if (options.individualHooks) {
return this.findAll({
where: options.where,
transaction: options.transaction,
logging: options.logging,
benchmark: options.benchmark,
paranoid: options.paranoid
}).then(_instances => {
instances = _instances;
if (!instances.length) {
return [];
}
valuesUse = values;
// Get instances and run beforeUpdate hook on each record individually
let instances;
let updateDoneRowByRow = false;
if (options.individualHooks) {
instances = await this.findAll({
where: options.where,
transaction: options.transaction,
logging: options.logging,
benchmark: options.benchmark,
paranoid: options.paranoid
});
// 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
let changedValues;
let different = false;
if (instances.length) {
// 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
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 => {
// Record updates in instances dataValues
Object.assign(instance.dataValues, values);
// Set the changed fields on the instance
_.forIn(valuesUse, (newValue, attr) => {
// Run beforeUpdate hook
await this.runHooks('beforeUpdate', instance, options);
if (!different) {
const thisChangedValues = {};
_.forIn(instance.dataValues, (newValue, attr) => {
if (newValue !== instance._previousDataValues[attr]) {
instance.setDataValue(attr, newValue);
thisChangedValues[attr] = newValue;
}
});
// Run beforeUpdate hook
return this.runHooks('beforeUpdate', instance, options).then(() => {
if (!different) {
const thisChangedValues = {};
_.forIn(instance.dataValues, (newValue, attr) => {
if (newValue !== instance._previousDataValues[attr]) {
thisChangedValues[attr] = newValue;
}
});
if (!changedValues) {
changedValues = thisChangedValues;
} else {
different = !_.isEqual(changedValues, thisChangedValues);
}
}
if (!changedValues) {
changedValues = thisChangedValues;
} else {
different = !_.isEqual(changedValues, thisChangedValues);
}
}
return instance;
}));
return instance;
});
})).then(_instances => {
instances = _instances;
if (!different) {
const keys = Object.keys(changedValues);
// Hooks do not change values or change them uniformly
if (keys.length) {
// Hooks change values - record changes in valuesUse so they are executed
valuesUse = changedValues;
options.fields = _.union(options.fields, keys);
}
return;
}
// 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];
}
if (!different) {
const keys = Object.keys(changedValues);
// Hooks do not change values or change them uniformly
if (keys.length) {
// Hooks change values - record changes in valuesUse so they are executed
valuesUse = changedValues;
options.fields = _.union(options.fields, keys);
}
} else {
instances = await Promise.all(instances.map(async instance => {
const individualOptions = _.clone(options);
delete individualOptions.individualHooks;
individualOptions.hooks = false;
individualOptions.validate = false;
// only updatedAt is being passed, then skip update
if (
_.isEmpty(valuesUse)
|| Object.keys(valuesUse).length === 1 && valuesUse[this._timestampAttributes.updatedAt]
) {
return [0];
return instance.save(individualOptions);
}));
updateDoneRowByRow = true;
}
}
}
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);
options = Utils.mapOptionFieldNames(options, this);
options.hasTrigger = this.options ? this.options.hasTrigger : false;
// Run query to update all rows
return this.QueryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, this.tableAttributes).then(affectedRows => {
if (options.returning) {
instances = affectedRows;
return [affectedRows.length, 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);
const affectedRows = await this.QueryInterface.bulkUpdate(this.getTableName(options), valuesUse, options.where, options, this.tableAttributes);
if (options.returning) {
result = [affectedRows.length, affectedRows];
instances = affectedRows;
} else {
result = [affectedRows];
}
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 {
*
* @returns {Promise} hash of attributes and their types
*/
static describe(schema, options) {
return this.QueryInterface.describeTable(this.tableName, Object.assign({ schema: schema || this._schema || undefined }, options));
static async describe(schema, options) {
return await this.QueryInterface.describeTable(this.tableName, Object.assign({ schema: schema || this._schema || undefined }, options));
}
static _getDefaultTimestamp(attr) {
......@@ -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
*/
static increment(fields, options) {
static async increment(fields, options) {
options = options || {};
if (typeof fields === 'string') fields = [fields];
if (Array.isArray(fields)) {
......@@ -3392,24 +3328,22 @@ class Model {
}
const tableName = this.getTableName(options);
let promise;
let affectedRows;
if (isSubtraction) {
promise = this.QueryInterface.decrement(
affectedRows = await this.QueryInterface.decrement(
this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options
);
} else {
promise = this.QueryInterface.increment(
affectedRows = await this.QueryInterface.increment(
this, tableName, where, incrementAmountsByField, extraAttributesToBeUpdated, options
);
}
return promise.then(affectedRows => {
if (options.returning) {
return [affectedRows, affectedRows.length];
}
if (options.returning) {
return [affectedRows, affectedRows.length];
}
return [affectedRows];
});
return [affectedRows];
}
/**
......@@ -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
*/
static decrement(fields, options) {
static async decrement(fields, options) {
options = _.defaults({ increment: false }, options, {
by: 1
});
return this.increment(fields, options);
return await this.increment(fields, options);
}
static _optionsMustContainWhere(options) {
......@@ -3863,7 +3797,7 @@ class Model {
*
* @returns {Promise<Model>}
*/
save(options) {
async save(options) {
if (arguments.length > 1) {
throw new Error('The second argument was removed in favor of the options object.');
}
......@@ -3938,59 +3872,49 @@ class Model {
this.dataValues[createdAtAttr] = this.constructor._getDefaultTimestamp(createdAtAttr) || now;
}
return Promise.resolve().then(() => {
// Validate
if (options.validate) {
return this.validate(options);
}
}).then(() => {
// Run before hook
if (options.hooks) {
const beforeHookValues = _.pick(this.dataValues, options.fields);
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 hookChanged;
let afterHookValues;
// Validate
if (options.validate) {
await this.validate(options);
}
// Run before hook
if (options.hooks) {
const beforeHookValues = _.pick(this.dataValues, options.fields);
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 hookChanged;
let afterHookValues;
if (updatedAtAttr && options.fields.includes(updatedAtAttr)) {
ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
}
if (updatedAtAttr && options.fields.includes(updatedAtAttr)) {
ignoreChanged = _.without(ignoreChanged, updatedAtAttr);
}
return this.constructor.runHooks(`before${hook}`, this, options)
.then(() => {
if (options.defaultFields && !this.isNewRecord) {
afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged));
await this.constructor.runHooks(`before${hook}`, this, options);
if (options.defaultFields && !this.isNewRecord) {
afterHookValues = _.pick(this.dataValues, _.difference(this.changed(), ignoreChanged));
hookChanged = [];
for (const key of Object.keys(afterHookValues)) {
if (afterHookValues[key] !== beforeHookValues[key]) {
hookChanged.push(key);
}
}
hookChanged = [];
for (const key of Object.keys(afterHookValues)) {
if (afterHookValues[key] !== beforeHookValues[key]) {
hookChanged.push(key);
}
}
options.fields = _.uniq(options.fields.concat(hookChanged));
}
options.fields = _.uniq(options.fields.concat(hookChanged));
}
if (hookChanged) {
if (options.validate) {
// Validate again
if (hookChanged) {
if (options.validate) {
// Validate again
options.skip = _.difference(Object.keys(this.constructor.rawAttributes), hookChanged);
return this.validate(options).then(() => {
delete options.skip;
});
}
}
});
options.skip = _.difference(Object.keys(this.constructor.rawAttributes), hookChanged);
await this.validate(options);
delete options.skip;
}
}
}).then(() => {
if (!options.fields.length) return this;
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 => {
}
if (options.fields.length && this.isNewRecord && this._options.include && this._options.include.length) {
await Promise.all(this._options.include.filter(include => include.association instanceof BelongsTo).map(async include => {
const instance = this.get(include.as);
if (!instance) return Promise.resolve();
if (!instance) return;
const includeOptions = _(Utils.cloneDeep(include))
.omit(['association'])
......@@ -4000,129 +3924,120 @@ class Model {
parentRecord: this
}).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));
if (!realFields.length) return this;
if (!this.changed() && !this.isNewRecord) return this;
}
const realFields = options.fields.filter(field => !this.constructor._virtualAttributes.has(field));
if (!realFields.length) return this;
if (!this.changed() && !this.isNewRecord) return this;
const versionFieldName = _.get(this.constructor.rawAttributes[versionAttr], 'field') || versionAttr;
let values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.constructor);
let query = null;
let args = [];
let where;
const versionFieldName = _.get(this.constructor.rawAttributes[versionAttr], 'field') || versionAttr;
let values = Utils.mapValueFieldNames(this.dataValues, options.fields, this.constructor);
let query = null;
let args = [];
let where;
if (this.isNewRecord) {
query = 'insert';
args = [this, this.constructor.getTableName(options), values, options];
if (this.isNewRecord) {
query = 'insert';
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 {
where = this.where(true);
if (versionAttr) {
values[versionFieldName] = parseInt(values[versionFieldName], 10) + 1;
}
query = 'update';
args = [this, this.constructor.getTableName(options), values, where, options];
result.dataValues[versionAttr] = values[versionFieldName];
}
}
return this.constructor.QueryInterface[query](...args)
.then(([result, rowsUpdated])=> {
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 {
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);
// 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);
// 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);
result.dataValues = Object.assign(result.dataValues, values);
if (wasNewRecord && this._options.include && this._options.include.length) {
await Promise.all(
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 (!Array.isArray(instances)) instances = [instances];
if (!instances.length) return Promise.resolve(Promise.resolve()).then(() => result);
if (!instances) return;
if (!Array.isArray(instances)) instances = [instances];
const includeOptions = _(Utils.cloneDeep(include))
.omit(['association'])
.defaults({
transaction: options.transaction,
logging: options.logging,
parentRecord: this
}).value();
const includeOptions = _(Utils.cloneDeep(include))
.omit(['association'])
.defaults({
transaction: options.transaction,
logging: options.logging,
parentRecord: this
}).value();
// Instances will be updated in place so we can safely treat HasOne like a HasMany
return Promise.resolve(Promise.all(instances.map(instance => {
if (include.association instanceof BelongsToMany) {
return Promise.resolve(instance.save(includeOptions).then(() => {
const values = {};
values[include.association.foreignKey] = this.get(this.constructor.primaryKeyAttribute, { raw: true });
values[include.association.otherKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association
Object.assign(values, include.association.through.scope);
if (instance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
attr === include.association.foreignKey ||
attr === include.association.otherKey ||
typeof instance[include.association.through.model.name][attr] === undefined) {
continue;
}
values[attr] = instance[include.association.through.model.name][attr];
}
// Instances will be updated in place so we can safely treat HasOne like a HasMany
await Promise.all(instances.map(async instance => {
if (include.association instanceof BelongsToMany) {
await instance.save(includeOptions);
const values0 = {};
values0[include.association.foreignKey] = this.get(this.constructor.primaryKeyAttribute, { raw: true });
values0[include.association.otherKey] = instance.get(instance.constructor.primaryKeyAttribute, { raw: true });
// Include values defined in the association
Object.assign(values0, include.association.through.scope);
if (instance[include.association.through.model.name]) {
for (const attr of Object.keys(include.association.through.model.rawAttributes)) {
if (include.association.through.model.rawAttributes[attr]._autoGenerated ||
attr === include.association.foreignKey ||
attr === include.association.otherKey ||
typeof instance[include.association.through.model.name][attr] === undefined) {
continue;
}
return Promise.resolve(include.association.throughModel.create(values, includeOptions)).then(() => result);
})).then(() => result);
values0[attr] = instance[include.association.through.model.name][attr];
}
}
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 });
Object.assign(instance, include.association.scope);
return Promise.resolve(instance.save(includeOptions)).then(() => result);
}))).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;
await instance.save(includeOptions);
}
}));
})
.then(result => {
for (const field of options.fields) {
result._previousDataValues[field] = result.dataValues[field];
this.changed(field, false);
}
this.isNewRecord = false;
return result;
});
});
);
}
// Run after hook
if (options.hooks) {
await this.constructor.runHooks(`after${hook}`, result, options);
}
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 {
*
* @returns {Promise<Model>}
*/
reload(options) {
async reload(options) {
options = Utils.defaults({}, options, {
where: this.where(),
include: this._options.include || null
});
return this.constructor.findOne(options)
.then(reload => {
if (!reload) {
throw new sequelizeErrors.InstanceError(
'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
this._options = reload._options;
// re-set instance values
this.set(reload.dataValues, {
raw: true,
reset: true && !options.attributes
});
return this;
});
const reloaded = await this.constructor.findOne(options);
if (!reloaded) {
throw new sequelizeErrors.InstanceError(
'Instance could not be reloaded because it does not exist anymore (find call returned null)'
);
}
// update the internal options of the instance
this._options = reloaded._options;
// re-set instance values
this.set(reloaded.dataValues, {
raw: true,
reset: true && !options.attributes
});
return this;
}
/**
......@@ -4177,8 +4088,8 @@ class Model {
*
* @returns {Promise}
*/
validate(options) {
return new InstanceValidator(this, options).validate();
async validate(options) {
return await new InstanceValidator(this, options).validate();
}
/**
......@@ -4195,7 +4106,7 @@ class 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
values = _.omitBy(values, value => value === undefined);
......@@ -4218,7 +4129,7 @@ class Model {
options.defaultFields = options.fields;
}
return this.save(options);
return await this.save(options);
}
/**
......@@ -4232,43 +4143,41 @@ class Model {
*
* @returns {Promise}
*/
destroy(options) {
async destroy(options) {
options = Object.assign({
hooks: true,
force: false
}, options);
return Promise.resolve().then(() => {
// Run before hook
if (options.hooks) {
return this.constructor.runHooks('beforeDestroy', this, options);
}
}).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());
}
// Run before hook
if (options.hooks) {
await this.constructor.runHooks('beforeDestroy', this, options);
}
const where = this.where(true);
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 => {
// Run after hook
if (options.hooks) {
return Promise.resolve(this.constructor.runHooks('afterDestroy', this, options)).then(() => result);
}
return result;
});
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));
}
// Run after hook
if (options.hooks) {
await this.constructor.runHooks('afterDestroy', this, options);
}
return result;
}
/**
......@@ -4300,7 +4209,7 @@ class Model {
*
* @returns {Promise}
*/
restore(options) {
async restore(options) {
if (!this.constructor._timestampAttributes.deletedAt) throw new Error('Model is not paranoid');
options = Object.assign({
......@@ -4308,25 +4217,22 @@ class Model {
force: false
}, options);
return Promise.resolve().then(() => {
// Run before hook
if (options.hooks) {
return this.constructor.runHooks('beforeRestore', this, options);
}
}).then(() => {
const deletedAtCol = this.constructor._timestampAttributes.deletedAt;
const deletedAtAttribute = this.constructor.rawAttributes[deletedAtCol];
const deletedAtDefaultValue = Object.prototype.hasOwnProperty.call(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null;
this.setDataValue(deletedAtCol, deletedAtDefaultValue);
return this.save(Object.assign({}, options, { hooks: false, omitNull: false }));
}).then(result => {
// Run after hook
if (options.hooks) {
return Promise.resolve(this.constructor.runHooks('afterRestore', this, options)).then(() => result);
}
// Run before hook
if (options.hooks) {
await this.constructor.runHooks('beforeRestore', this, options);
}
const deletedAtCol = this.constructor._timestampAttributes.deletedAt;
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 }));
// Run after hook
if (options.hooks) {
await this.constructor.runHooks('afterRestore', this, options);
return result;
});
}
return result;
}
/**
......@@ -4360,14 +4266,16 @@ class Model {
* @returns {Promise<Model>}
* @since 4.0.0
*/
increment(fields, options) {
async increment(fields, options) {
const identifier = this.where();
options = Utils.cloneDeep(options);
options.where = Object.assign({}, options.where, identifier);
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 {
*
* @returns {Promise}
*/
decrement(fields, options) {
async decrement(fields, options) {
options = _.defaults({ increment: false }, options, {
by: 1
});
return this.increment(fields, options);
return await this.increment(fields, options);
}
/**
......
......@@ -651,10 +651,9 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
});
describe('restore', () => {
it('returns an error if the model is not paranoid', function() {
return this.User.create({ username: 'Peter', secretValue: '42' }).then(user => {
expect(() => {user.restore();}).to.throw(Error, 'Model is not paranoid');
});
it('returns an error if the model is not paranoid', async function() {
const user = await this.User.create({ username: 'Peter', secretValue: '42' });
await expect(user.restore()).to.be.rejectedWith(Error, 'Model is not paranoid');
});
it('restores a previously deleted model', function() {
......
......@@ -1555,11 +1555,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
});
describe('restore', () => {
it('synchronously throws an error if the model is not paranoid', async function() {
expect(() => {
this.User.restore({ where: { secretValue: '42' } });
throw new Error('Did not throw synchronously');
}).to.throw(Error, 'Model is not paranoid');
it('rejects with an error if the model is not paranoid', async function() {
await expect(this.User.restore({ where: { secretValue: '42' } })).to.be.rejectedWith(Error, 'Model is not paranoid');
});
it('restores a previously deleted model', async function() {
......
......@@ -18,14 +18,14 @@ describe(Support.getTestDialectTeaser('Model'), () => {
count: Sequelize.BIGINT
});
it('should reject if options are missing', () => {
return expect(() => Model.increment(['id', 'count']))
.to.throw('Missing where attribute in the options parameter');
it('should reject if options are missing', async () => {
await expect(Model.increment(['id', 'count']))
.to.be.rejectedWith('Missing where attribute in the options parameter');
});
it('should reject if options.where are missing', () => {
return expect(() => Model.increment(['id', 'count'], { by: 10 }))
.to.throw('Missing where attribute in the options parameter');
it('should reject if options.where are missing', async () => {
await expect(Model.increment(['id', 'count'], { by: 10 }))
.to.be.rejectedWith('Missing where attribute in the options parameter');
});
});
});
......
......@@ -9,15 +9,13 @@ const chai = require('chai'),
describe(Support.getTestDialectTeaser('Instance'), () => {
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', {
}),
instance = Model.build({}, { isNewRecord: false });
expect(() => {
instance.save();
}).to.throw();
await expect(instance.save()).to.be.rejected;
});
describe('options tests', () => {
......
......@@ -35,13 +35,10 @@ describe(Support.getTestDialectTeaser('Model'), () => {
this.stubDelete.restore();
});
it('can detect complex objects', () => {
it('can detect complex objects', async () => {
const Where = function() { this.secretValue = '1'; };
expect(() => {
User.destroy({ where: new Where() });
}).to.throw();
await expect(User.destroy({ where: new Where() })).to.be.rejected;
});
});
});
......@@ -65,9 +65,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
expect(this.warnOnInvalidOptionsStub.calledOnce).to.equal(true);
});
it('Throws an error when the attributes option is formatted incorrectly', () => {
const errorFunction = Model.findAll.bind(Model, { attributes: 'name' });
expect(errorFunction).to.throw(sequelizeErrors.QueryError);
it('Throws an error when the attributes option is formatted incorrectly', async () => {
await expect(Model.findAll({ attributes: 'name' })).to.be.rejectedWith(sequelizeErrors.QueryError);
});
});
......
......@@ -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'; };
expect(() => {
this.User.update(this.updates, { where: new Where() });
}).to.throw();
await expect(this.User.update(this.updates, { where: new Where() })).to.be.rejected;
});
});
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!