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 8, 2024
1 parent 75a3d90 commit 1c4e81d
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 215 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Escapeable extends Bindable {

export interface QueryWithBindParams {
query: string;
bind?: BindOrReplacements | undefined;
bind?: Record<string, unknown> | undefined;
}

// keep CREATE_DATABASE_QUERY_SUPPORTABLE_OPTIONS updated when modifying this
Expand Down
82 changes: 82 additions & 0 deletions packages/core/src/dialects/abstract/query-interface-typescript.ts
Original file line number Diff line number Diff line change
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 { ModelStatic, NormalizedAttributeOptions } 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 { AbstractQueryGenerator } from './query-generator';
Expand All @@ -38,6 +41,7 @@ import type {
QiListSchemasOptions,
QiListTablesOptions,
QiTruncateTableOptions,
QiUpdateOptions,
RemoveColumnOptions,
RemoveConstraintOptions,
RenameTableOptions,
Expand Down Expand Up @@ -737,4 +741,82 @@ export class AbstractQueryInterfaceTypeScript {

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, NormalizedAttributeOptions>,
): 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, NormalizedAttributeOptions>,
): Promise<[M | Record<string, unknown>, number]> {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

let columnDefinitions: Record<string, NormalizedAttributeOptions> | 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
Original file line number Diff line number Diff line change
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, Sequelize } from '../../sequelize';
import type { IsolationLevel, Transaction } from '../../transaction';
import type { AllowLowercase } from '../../utils/types.js';
Expand Down Expand Up @@ -39,10 +31,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 @@ -357,28 +345,6 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript {
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
69 changes: 0 additions & 69 deletions packages/core/src/dialects/abstract/query-interface.js
Original file line number Diff line number Diff line change
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,73 +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,
modelDefinition && getObjectFromMap(modelDefinition.attributes),
);

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.getTableName(options), attrValueHash, Object.assign(where, options.where), options, getObjectFromMap(modelDefinition.attributes));
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.getTableName(options), attrValueHash, options.where, options, getObjectFromMap(modelDefinition.attributes));
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.getTableName(options), valuesUse, options.where, options, getObjectFromMap(this.modelDefinition.physicalAttributes));
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.getTableName(options), 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 1c4e81d

Please sign in to comment.