Skip to content

Commit

Permalink
feat: migrate update & bulkUpdate to ts
Browse files Browse the repository at this point in the history
  • Loading branch information
lohart13 committed Feb 10, 2024
1 parent f72e8d1 commit 2cf0c07
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 209 deletions.
82 changes: 82 additions & 0 deletions packages/core/src/dialects/abstract/query-interface-typescript.ts
Expand Up @@ -3,6 +3,8 @@ import isEmpty from 'lodash/isEmpty';
import { Deferrable } from '../../deferrable';
import type { ConstraintChecking } from '../../deferrable';
import { BaseError } from '../../errors';
import type { AttributeOptions, ModelStatic } from '../../model';
import { Model } from '../../model';
import { setTransactionFromCls } from '../../model-internals.js';
import { QueryTypes } from '../../query-types';
import type { QueryRawOptions, QueryRawOptionsWithType, Sequelize } from '../../sequelize';
Expand All @@ -13,6 +15,7 @@ import {
showAllToListSchemas,
showAllToListTables,
} from '../../utils/deprecations';
import { assertNoReservedBind, combineBinds } from '../../utils/sql';
import type { RequiredBy } from '../../utils/types';
import type { Connection } from './connection-manager.js';
import type { TableNameOrModel } from './query-generator-typescript.js';
Expand All @@ -37,6 +40,7 @@ import type {
QiListSchemasOptions,
QiListTablesOptions,
QiTruncateTableOptions,
QiUpdateOptions,
RemoveColumnOptions,
RemoveConstraintOptions,
RenameTableOptions,
Expand Down Expand Up @@ -741,4 +745,82 @@ export class AbstractQueryInterfaceTypeScript<Dialect extends AbstractDialect =

return this.sequelize.queryRaw(sql, { ...bulkDeleteOptions, raw: true, type: QueryTypes.DELETE });
}

/**
* Update multiple records of a table
*
* @example
* queryInterface.bulkUpdate('roles', {
* label: 'admin',
* }, {
* userType: 3,
* },
* );
*
* @param tableName Table name to update
* @param values Values to be inserted, mapped to field name
* @param options Various options, please see Model.bulkCreate options
* @param columnDefinitions Attributes on return objects if supported by SQL dialect
*/
async bulkUpdate(
tableName: TableNameOrModel,
values: Record<string, unknown>,
options?: QiUpdateOptions,
columnDefinitions?: Record<string, AttributeOptions>,
): Promise<number> {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

const bulkUpdateOptions = { ...options };
const { bind, query } = this.queryGenerator.updateQuery(tableName, values, bulkUpdateOptions, columnDefinitions);

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete bulkUpdateOptions.replacements;
bulkUpdateOptions.bind = combineBinds(bulkUpdateOptions.bind ?? {}, bind ?? {});

return this.sequelize.queryRaw(query, { ...bulkUpdateOptions, type: QueryTypes.BULKUPDATE });
}

/**
* Updates a row
*
* @param tableName
* @param values
* @param options
* @param instanceOrColumnDefinitions
*/
async update<M extends Model>(
tableName: TableNameOrModel,
values: Record<string, unknown>,
options?: QiUpdateOptions,
instanceOrColumnDefinitions?: M | Record<string, AttributeOptions>,
): Promise<[M | Record<string, unknown>, number]> {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

let columnDefinitions: Record<string, AttributeOptions> | undefined;
const updateOptions = { ...options };
if (instanceOrColumnDefinitions instanceof Model) {
const model = (instanceOrColumnDefinitions.constructor as ModelStatic<M>);
updateOptions.model = model;
updateOptions.instance = instanceOrColumnDefinitions;
updateOptions.hasTrigger = model.modelDefinition?.options.hasTrigger ?? false;
} else {
columnDefinitions = instanceOrColumnDefinitions;
}

const { query, bind } = this.queryGenerator.updateQuery(tableName, values, options, columnDefinitions);

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete updateOptions.replacements;
updateOptions.bind = combineBinds(updateOptions.bind ?? {}, bind ?? {});

if (instanceOrColumnDefinitions instanceof Model) {
return this.sequelize.queryRaw<M>(query, { ...updateOptions, type: QueryTypes.UPDATE });
}

return this.sequelize.queryRaw(query, { ...options, type: QueryTypes.UPDATE });
}
}
36 changes: 1 addition & 35 deletions packages/core/src/dialects/abstract/query-interface.d.ts
Expand Up @@ -2,15 +2,7 @@ import type { SetRequired } from 'type-fest';
import type { Col } from '../../expression-builders/col.js';
import type { Fn } from '../../expression-builders/fn.js';
import type { Literal } from '../../expression-builders/literal.js';
import type {
AttributeOptions,
Attributes,
CreationAttributes,
Filterable,
Model,
ModelStatic,
NormalizedAttributeOptions,
} from '../../model';
import type { AttributeOptions, Attributes, CreationAttributes, Filterable, Model, ModelStatic } from '../../model';
import type { QueryRawOptions, QueryRawOptionsWithModel } from '../../sequelize';
import type { IsolationLevel, Transaction } from '../../transaction';
import type { AllowLowercase } from '../../utils/types.js';
Expand Down Expand Up @@ -40,10 +32,6 @@ export interface QiSelectOptions extends QueryRawOptions, Filterable<any>, AddLi
minifyAliases?: boolean;
}

export interface QiUpdateOptions extends QueryRawOptions, Replaceable {
returning?: boolean | Array<string | Literal | Col>;
}

export interface QiArithmeticOptions extends QueryRawOptions, Replaceable {
returning?: boolean | Array<string | Literal | Col>;
}
Expand Down Expand Up @@ -347,28 +335,6 @@ export class AbstractQueryInterface<Dialect extends AbstractDialect = AbstractDi
attributes?: Record<string, AttributeOptions>
): Promise<object | number>;

/**
* Updates a row
*/
update<M extends Model>(
instance: M,
tableName: TableName,
values: object,
where: WhereOptions<Attributes<M>>,
options?: QiUpdateOptions
): Promise<object>;

/**
* Updates multiple rows at once
*/
bulkUpdate(
tableName: TableName,
values: object,
where: WhereOptions<any>,
options?: QiOptionsWithReplacements,
columnDefinitions?: { [columnName: string]: NormalizedAttributeOptions },
): Promise<object>;

/**
* Returns selected rows
*/
Expand Down
64 changes: 0 additions & 64 deletions packages/core/src/dialects/abstract/query-interface.js
Expand Up @@ -7,9 +7,7 @@ import { AbstractDataType } from './data-types';
import { AbstractQueryInterfaceTypeScript } from './query-interface-typescript';

import defaults from 'lodash/defaults';
import find from 'lodash/find';
import intersection from 'lodash/intersection';
import isObject from 'lodash/isObject';
import mapValues from 'lodash/mapValues';
import uniq from 'lodash/uniq';

Expand Down Expand Up @@ -475,68 +473,6 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript {
return results[0];
}

async update(instance, tableName, values, where, options) {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

const modelDefinition = instance?.constructor.modelDefinition;

options = { ...options, model: instance?.constructor, where };
options.hasTrigger = modelDefinition?.options.hasTrigger;

const { query, bind } = this.queryGenerator.updateQuery(tableName, values, options);

options.type = QueryTypes.UPDATE;
options.instance = instance;

delete options.replacements;

options.bind = combineBinds(options.bind, bind);

return await this.sequelize.queryRaw(query, options);
}

/**
* Update multiple records of a table
*
* @example
* queryInterface.bulkUpdate('roles', {
* label: 'admin',
* }, {
* userType: 3,
* },
* );
*
* @param {string} tableName Table name to update
* @param {object} values Values to be inserted, mapped to field name
* @param {object} where A hash with conditions OR an ID as integer OR a string with conditions
* @param {object} [options] Various options, please see Model.bulkCreate options
* @param {object} [columnDefinitions] Attributes on return objects if supported by SQL dialect
*
* @returns {Promise}
*/
async bulkUpdate(tableName, values, where, options, columnDefinitions) {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

options = cloneDeep(options) ?? {};
if (typeof where === 'object') {
where = cloneDeep(where) ?? {};
}

const { bind, query } = this.queryGenerator.updateQuery(tableName, values, { ...options, where }, columnDefinitions);
const table = isObject(tableName) ? tableName : { tableName };
const model = options.model ? options.model : find(this.sequelize.modelManager.models, { tableName: table.tableName });

options.type = QueryTypes.BULKUPDATE;
options.model = model;
options.bind = combineBinds(options.bind, bind);

return await this.sequelize.queryRaw(query, options);
}

async select(model, tableName, optionsArg) {
const minifyAliases = optionsArg.minifyAliases ?? this.sequelize.options.minifyAliases;
const options = { ...optionsArg, type: QueryTypes.SELECT, model, minifyAliases };
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/dialects/abstract/query-interface.types.ts
Expand Up @@ -15,6 +15,7 @@ import type {
RenameTableQueryOptions,
ShowConstraintsQueryOptions,
TruncateTableQueryOptions,
UpdateQueryOptions,
} from './query-generator.types';

export interface DatabaseDescription {
Expand Down Expand Up @@ -142,3 +143,6 @@ export interface ShowConstraintsOptions extends ShowConstraintsQueryOptions, Que

/** Options accepted by {@link AbstractQueryInterface#bulkDelete} */
export interface BulkDeleteOptions extends BulkDeleteQueryOptions, QueryRawOptions { }

/** Options accepted by {@link AbstractQueryInterface#update} */
export interface QiUpdateOptions extends UpdateQueryOptions, QueryRawOptions { }
19 changes: 9 additions & 10 deletions packages/core/src/model.js
Expand Up @@ -2520,20 +2520,19 @@ ${associationOwner._getAssociationDebugList()}`);
// TODO: rename force -> paranoid: false, as that's how it's called in the instance version
// Run delete query (or update if paranoid)
if (modelDefinition.timestampAttributeNames.deletedAt && !options.force) {
// Set query type appropriately when running soft delete
options.type = QueryTypes.BULKUPDATE;

const attrValueHash = {};
const deletedAtAttribute = attributes.get(modelDefinition.timestampAttributeNames.deletedAt);
const deletedAtColumnName = deletedAtAttribute.columnName;

// FIXME: where must be joined with AND instead of using Object.assign. This won't work with literals!
const where = {
[deletedAtColumnName]: Object.hasOwn(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null,
options.where = {
[Op.and]: [
options.where,
{ [deletedAtColumnName]: Object.hasOwn(deletedAtAttribute, 'defaultValue') ? deletedAtAttribute.defaultValue : null },
],
};

attrValueHash[deletedAtColumnName] = new Date();
result = await this.queryInterface.bulkUpdate(this, attrValueHash, Object.assign(where, options.where), options);
result = await this.queryInterface.bulkUpdate(this, attrValueHash, options);
} else {
result = await this.queryInterface.bulkDelete(this, options);
}
Expand Down Expand Up @@ -2612,7 +2611,7 @@ ${associationOwner._getAssociationDebugList()}`);

attrValueHash[deletedAtAttribute.columnName || deletedAtAttributeName] = deletedAtDefaultValue;
options.omitNull = false;
const result = await this.queryInterface.bulkUpdate(this, attrValueHash, options.where, options);
const result = await this.queryInterface.bulkUpdate(this, attrValueHash, options);
// Run afterDestroy hook on each record individually
if (options.individualHooks) {
await Promise.all(
Expand Down Expand Up @@ -2805,7 +2804,7 @@ ${associationOwner._getAssociationDebugList()}`);
options = mapOptionFieldNames(options, this);
options.hasTrigger = this.options ? this.options.hasTrigger : false;

const affectedRows = await this.queryInterface.bulkUpdate(this, valuesUse, options.where, options);
const affectedRows = await this.queryInterface.bulkUpdate(this, valuesUse, options);
if (options.returning) {
result = [affectedRows.length, affectedRows];
instances = affectedRows;
Expand Down Expand Up @@ -3750,7 +3749,7 @@ Instead of specifying a Model, either:
}

query = 'update';
args = [this, this.constructor, values, where, options];
args = [this.constructor, values, { ...options, where }, this];
}

if (!this.changed() && !this.isNewRecord) {
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/sequelize.d.ts
Expand Up @@ -949,7 +949,7 @@ export class Sequelize extends SequelizeTypeScript {
* @param options Query options
*/
/* eslint-disable max-len -- these signatures are more readable if they are all aligned */
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPDATE>): Promise<[undefined, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPDATE>): Promise<[Record<string, unknown>, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.BULKUPDATE>): Promise<number>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.INSERT>): Promise<[number, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPSERT>): Promise<number>;
Expand All @@ -960,6 +960,7 @@ export class Sequelize extends SequelizeTypeScript {
query<M extends Model>(sql: string | BaseSqlExpression, options: QueryOptionsWithModel<M>): Promise<M[]>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.SELECT> & { plain: true }): Promise<T | null>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.SELECT>): Promise<T[]>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPDATE>): Promise<[T, number]>;
query(sql: string | BaseSqlExpression, options: (QueryOptions | QueryOptionsWithType<QueryTypes.RAW>) & { plain: true }): Promise<{ [key: string]: unknown } | null>;
query(sql: string | BaseSqlExpression, options?: QueryOptions | QueryOptionsWithType<QueryTypes.RAW>): Promise<[unknown[], unknown]>;

Expand All @@ -969,7 +970,7 @@ export class Sequelize extends SequelizeTypeScript {
* @param sql The SQL to execute
* @param options The options for the query. See {@link QueryRawOptions} for details.
*/
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPDATE>): Promise<[undefined, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPDATE>): Promise<[Record<string, unknown>, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.BULKUPDATE>): Promise<number>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.INSERT>): Promise<[number, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPSERT>): Promise<number>;
Expand All @@ -980,6 +981,7 @@ export class Sequelize extends SequelizeTypeScript {
queryRaw<M extends Model>(sql: string, options: QueryRawOptionsWithModel<M>): Promise<M[]>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.SELECT> & { plain: true }): Promise<T | null>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.SELECT>): Promise<T[]>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPDATE>): Promise<[T, number]>;
queryRaw(sql: string, options: (QueryRawOptions | QueryRawOptionsWithType<QueryTypes.RAW>) & { plain: true }): Promise<{ [key: string]: unknown } | null>;
queryRaw(sql: string, options?: QueryRawOptions | QueryRawOptionsWithType<QueryTypes.RAW>): Promise<[unknown[], unknown]>;
/* eslint-enable max-len */
Expand Down
Expand Up @@ -604,7 +604,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
const tableName = User.getTableName();

await expect(
user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id }),
user.sequelize.queryInterface.update(tableName, { id: 999 }, { where: { id: user.id } }, user),
).to.eventually.be.rejectedWith(Sequelize.ForeignKeyConstraintError);

// Should fail due to FK restriction
Expand Down Expand Up @@ -632,7 +632,7 @@ describe(Support.getTestDialectTeaser('BelongsTo'), () => {
// `WHERE` clause

const tableName = User.getTableName();
await user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id });
await user.sequelize.queryInterface.update(tableName, { id: 999 }, { where: { id: user.id } }, user);
const tasks = await Task.findAll();
expect(tasks).to.have.length(1);
expect(tasks[0].userId).to.equal(999);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/test/integration/associations/has-many.test.js
Expand Up @@ -1096,7 +1096,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
// `WHERE` clause

const tableName = User.getTableName();
await user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id });
await user.sequelize.queryInterface.update(tableName, { id: 999 }, { where: { id: user.id } }, user);
const tasks = await Task.findAll();
expect(tasks).to.have.length(1);
expect(tasks[0].userId).to.equal(999);
Expand Down Expand Up @@ -1157,7 +1157,7 @@ describe(Support.getTestDialectTeaser('HasMany'), () => {
const tableName = User.getTableName();

try {
tasks = await user.sequelize.queryInterface.update(user, tableName, { id: 999 }, { id: user.id });
tasks = await user.sequelize.queryInterface.update(tableName, { id: 999 }, { where: { id: user.id } }, user);
} catch (error) {
if (!(error instanceof Sequelize.ForeignKeyConstraintError)) {
throw error;
Expand Down

0 comments on commit 2cf0c07

Please sign in to comment.