Skip to content

Commit 903c7f4

Browse files
authored
Merge pull request #1896 from rocket-admin/backend_bulk_delete_optimisation
feat: implement bulk deletion for tables with composite primary keys
2 parents c80c597 + 38a638d commit 903c7f4

22 files changed

Lines changed: 906 additions & 138 deletions

‎backend/src/entities/table-logs/table-logs.service.ts‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { InjectRepository } from '@nestjs/typeorm';
33
import { findSensitiveValues, scrub } from '@zapier/secret-scrubber';
4-
import PQueue from 'p-queue';
54
import { Repository } from 'typeorm';
65
import { LogOperationTypeEnum } from '../../enums/log-operation-type.enum.js';
76
import { OperationResultStatusEnum } from '../../enums/operation-result-status.enum.js';
@@ -17,6 +16,9 @@ import { TableLogsEntity } from './table-logs.entity.js';
1716
import { buildCreatedLogRecord } from './utils/build-created-log-record.js';
1817
import { buildTableLogsEntity } from './utils/build-table-logs-entity.js';
1918

19+
/** Rows per INSERT when a bulk operation writes its audit log; keeps one statement well under Postgres' bind limit. */
20+
const TABLE_LOGS_INSERT_CHUNK_SIZE = 500;
21+
2022
@Injectable()
2123
export class TableLogsService {
2224
constructor(
@@ -220,16 +222,13 @@ export class TableLogsService {
220222
const newLogRecord = buildTableLogsEntity(loggedData, email);
221223
tableLogsEntities.push(newLogRecord);
222224
}
223-
const queue = new PQueue({ concurrency: 2 });
224-
const createdLogs: Array<CreatedLogRecordDs | undefined> = await Promise.all(
225-
tableLogsEntities.map(async (newLogRecord) => {
226-
return await queue.add(async () => {
227-
const savedLogRecord = await this.tableLogsRepository.save(newLogRecord);
228-
return buildCreatedLogRecord(savedLogRecord);
229-
});
230-
}),
231-
);
232-
return createdLogs.filter((log) => log) as Array<CreatedLogRecordDs>;
225+
if (tableLogsEntities.length === 0) {
226+
return [];
227+
}
228+
const savedLogRecords = await this.tableLogsRepository.save(tableLogsEntities, {
229+
chunk: TABLE_LOGS_INSERT_CHUNK_SIZE,
230+
});
231+
return savedLogRecords.map((savedLogRecord) => buildCreatedLogRecord(savedLogRecord));
233232
}
234233

235234
private async isTableAuditEnabledInConnectionProperties(connectionId: string): Promise<boolean> {

‎backend/src/entities/table/table-pure-crud-operations/use-cases/pure-delete-row-from-table.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class PureDeleteRowFromTableUseCase
6464
const availablePrimaryColumns = primaryColumns.map((column) => column.column_name);
6565
for (const key in primaryKey) {
6666
// eslint-disable-next-line security/detect-object-injection
67-
if (!primaryKey[key] && primaryKey[key] !== '') delete primaryKey[key];
67+
if (primaryKey[key] === undefined) delete primaryKey[key];
6868
}
6969
const receivedPrimaryColumns = Object.keys(primaryKey);
7070
if (!compareArrayElements(availablePrimaryColumns, receivedPrimaryColumns)) {

‎backend/src/entities/table/table-pure-crud-operations/use-cases/pure-read-row-from-table.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class PureReadRowFromTableUseCase
6464
const availablePrimaryColumns = primaryColumns.map((column) => column.column_name);
6565
for (const key in primaryKey) {
6666
// eslint-disable-next-line security/detect-object-injection
67-
if (!primaryKey[key] && primaryKey[key] !== '') delete primaryKey[key];
67+
if (primaryKey[key] === undefined) delete primaryKey[key];
6868
}
6969
const receivedPrimaryColumns = Object.keys(primaryKey);
7070
if (!compareArrayElements(availablePrimaryColumns, receivedPrimaryColumns)) {

‎backend/src/entities/table/table-pure-crud-operations/use-cases/pure-update-row-in-table.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class PureUpdateRowInTableUseCase
6767
const availablePrimaryColumns = tablePrimaryKeys.map((key) => key.column_name);
6868
for (const key in primaryKey) {
6969
// eslint-disable-next-line security/detect-object-injection
70-
if (!primaryKey[key] && primaryKey[key] !== '') delete primaryKey[key];
70+
if (primaryKey[key] === undefined) delete primaryKey[key];
7171
}
7272
const receivedPrimaryColumns = Object.keys(primaryKey);
7373
if (!compareArrayElements(availablePrimaryColumns, receivedPrimaryColumns)) {

‎backend/src/entities/table/table.controller.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ export class TableController {
565565
})
566566
@ApiQuery({ name: 'tableName', required: true })
567567
@UseGuards(TableDeleteGuard)
568+
@Timeout(TimeoutDefaults.EXTENDED)
568569
@Put('/table/rows/delete/:connectionId')
569570
async deleteRowsInTable(
570571
@MasterPassword() masterPwd: string,

‎backend/src/entities/table/use-cases/delete-row-from-table.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class DeleteRowFromTableUseCase
8080

8181
Object.keys(primaryKey).forEach((key) => {
8282
// eslint-disable-next-line security/detect-object-injection
83-
if (!primaryKey[key] && primaryKey[key] !== '') {
83+
if (primaryKey[key] === undefined) {
8484
// eslint-disable-next-line security/detect-object-injection
8585
delete primaryKey[key];
8686
}

‎backend/src/entities/table/use-cases/delete-rows-from-table.use.case.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { TableLogsService } from '../../table-logs/table-logs.service.js';
1717
import { DeleteRowsFromTableDs } from '../application/data-structures/delete-row-from-table.ds.js';
1818
import { buildCommonTableSettingsInput } from '../utils/build-common-table-settings-input.util.js';
1919
import { convertHexDataInPrimaryKeyUtil } from '../utils/convert-hex-data-in-primary-key.util.js';
20-
import { findObjectsWithProperties } from '../utils/find-objects-with-properties.js';
2120
import { getUserEmailForAgent, validateConnection } from '../utils/validate-connection.util.js';
2221
import { IDeleteRowsFromTable } from './table-use-cases.interface.js';
2322

@@ -87,7 +86,7 @@ export class DeleteRowsFromTableUseCase
8786
primaryKeys.forEach((primaryKey) => {
8887
Object.keys(primaryKey).forEach((key) => {
8988
// eslint-disable-next-line security/detect-object-injection
90-
if (!primaryKey[key] && primaryKey[key] !== '') {
89+
if (primaryKey[key] === undefined) {
9190
// eslint-disable-next-line security/detect-object-injection
9291
delete primaryKey[key];
9392
}
@@ -124,6 +123,7 @@ export class DeleteRowsFromTableUseCase
124123
);
125124
}
126125

126+
const oldRowsByPrimaryKey = indexRowsByPrimaryKey(oldRowsData, availablePrimaryColumns);
127127
const deleteOperationsResults: Array<DeleteRowsFromTableResult> = [];
128128

129129
try {
@@ -132,7 +132,7 @@ export class DeleteRowsFromTableUseCase
132132
deleteOperationsResults.push({
133133
operationStatusResult: OperationResultStatusEnum.successfully,
134134
row: primaryKey,
135-
old_data: findObjectsWithProperties(oldRowsData, primaryKey).at(0) ?? {},
135+
old_data: oldRowsByPrimaryKey.get(primaryKeyIndexKey(primaryKey, availablePrimaryColumns)) ?? {},
136136
error: null,
137137
affected_primary_key: primaryKey as unknown as string,
138138
});
@@ -143,7 +143,7 @@ export class DeleteRowsFromTableUseCase
143143
deleteOperationsResults.push({
144144
operationStatusResult: OperationResultStatusEnum.unsuccessfully,
145145
row: primaryKey,
146-
old_data: findObjectsWithProperties(oldRowsData, primaryKey).at(0) ?? {},
146+
old_data: oldRowsByPrimaryKey.get(primaryKeyIndexKey(primaryKey, availablePrimaryColumns)) ?? {},
147147
error: getErrorMessage(error),
148148
affected_primary_key: primaryKey as unknown as string,
149149
});
@@ -166,3 +166,19 @@ export class DeleteRowsFromTableUseCase
166166
}
167167
}
168168
}
169+
170+
function primaryKeyIndexKey(row: Record<string, unknown>, primaryColumns: Array<string>): string {
171+
// eslint-disable-next-line security/detect-object-injection
172+
return primaryColumns.map((column) => String(row[column])).join('\u0000');
173+
}
174+
175+
function indexRowsByPrimaryKey(
176+
rows: Array<Record<string, unknown>>,
177+
primaryColumns: Array<string>,
178+
): Map<string, Record<string, unknown>> {
179+
const index = new Map<string, Record<string, unknown>>();
180+
for (const row of rows) {
181+
index.set(primaryKeyIndexKey(row, primaryColumns), row);
182+
}
183+
return index;
184+
}

‎backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class GetRowByPrimaryKeyUseCase
100100
const availablePrimaryColumns: Array<string> = tablePrimaryKeys.map((column) => column.column_name);
101101
for (const key in primaryKey) {
102102
// eslint-disable-next-line security/detect-object-injection
103-
if (!primaryKey[key] && primaryKey[key] !== '') delete primaryKey[key];
103+
if (primaryKey[key] === undefined) delete primaryKey[key];
104104
}
105105
const receivedPrimaryColumns = Object.keys(primaryKey);
106106
if (!compareArrayElements(availablePrimaryColumns, receivedPrimaryColumns)) {

‎backend/src/entities/table/use-cases/update-row-in-table.use.case.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export class UpdateRowInTableUseCase
172172
});
173173
for (const key in primaryKey) {
174174
// eslint-disable-next-line security/detect-object-injection
175-
if (!primaryKey[key] && primaryKey[key] !== '') delete primaryKey[key];
175+
if (primaryKey[key] === undefined) delete primaryKey[key];
176176
}
177177
const receivedPrimaryColumns = Object.keys(primaryKey);
178178

‎backend/src/entities/table/utils/add-display-names-for-tables.util.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { TableSettingsEntity } from '../../table-settings/common-table-settings/table-settings.entity.js';
21
import { ITableAndViewPermissionData } from '../../permission/permission.interface.js';
2+
import { TableSettingsEntity } from '../../table-settings/common-table-settings/table-settings.entity.js';
33
import { FoundTableDs } from '../application/data-structures/found-table.ds.js';
44

55
export function addDisplayNamesForTables(

0 commit comments

Comments
 (0)