diff --git a/packages/@n8n/api-types/src/dto/data-store/delete-data-store-rows-query.dto.ts b/packages/@n8n/api-types/src/dto/data-store/delete-data-store-rows-query.dto.ts deleted file mode 100644 index 841e065d5b..0000000000 --- a/packages/@n8n/api-types/src/dto/data-store/delete-data-store-rows-query.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from 'zod'; -import { Z } from 'zod-class'; - -export class DeleteDataStoreRowsQueryDto extends Z.class({ - ids: z - .string() - .transform((str) => { - if (!str.trim()) return []; - return str.split(',').map((id) => parseInt(id.trim(), 10)); - }) - .refine((ids) => ids.length === 0 || ids.every((id) => !isNaN(id) && id > 0), { - message: 'All ids must be positive integers', - }), -}) {} diff --git a/packages/@n8n/api-types/src/dto/data-store/delete-data-table-rows.dto.ts b/packages/@n8n/api-types/src/dto/data-store/delete-data-table-rows.dto.ts new file mode 100644 index 0000000000..8274b5d907 --- /dev/null +++ b/packages/@n8n/api-types/src/dto/data-store/delete-data-table-rows.dto.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { Z } from 'zod-class'; + +import { dataTableFilterSchema } from '../../schemas/data-table-filter.schema'; + +const deleteDataTableRowsShape = { + filter: dataTableFilterSchema.optional(), + returnData: z.boolean().optional().default(false), +}; + +export class DeleteDataTableRowsDto extends Z.class(deleteDataTableRowsShape) {} diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index aa0d29eac5..681a087cfd 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -86,6 +86,7 @@ export { OidcConfigDto } from './oidc/config.dto'; export { CreateDataStoreDto } from './data-store/create-data-store.dto'; export { UpdateDataStoreDto } from './data-store/update-data-store.dto'; export { UpdateDataTableRowDto } from './data-store/update-data-store-row.dto'; +export { DeleteDataTableRowsDto } from './data-store/delete-data-table-rows.dto'; export { UpsertDataStoreRowDto } from './data-store/upsert-data-store-row.dto'; export { ListDataStoreQueryDto } from './data-store/list-data-store-query.dto'; export { ListDataStoreContentQueryDto } from './data-store/list-data-store-content-query.dto'; @@ -93,4 +94,3 @@ export { CreateDataStoreColumnDto } from './data-store/create-data-store-column. export { AddDataStoreRowsDto } from './data-store/add-data-store-rows.dto'; export { AddDataStoreColumnDto } from './data-store/add-data-store-column.dto'; export { MoveDataStoreColumnDto } from './data-store/move-data-store-column.dto'; -export { DeleteDataStoreRowsQueryDto } from './data-store/delete-data-store-rows-query.dto'; diff --git a/packages/cli/src/modules/data-table/__tests__/data-store.controller.test.ts b/packages/cli/src/modules/data-table/__tests__/data-store.controller.test.ts index b76ca001de..cd1cceb150 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-store.controller.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-store.controller.test.ts @@ -2505,7 +2505,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { test('should not delete rows when project does not exist', async () => { await authOwnerAgent .delete('/projects/non-existing-id/data-tables/some-data-store-id/rows') - .query({ ids: '1,2' }) .expect(403); }); @@ -2514,7 +2513,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authOwnerAgent .delete(`/projects/${project.id}/data-tables/non-existing-id/rows`) - .query({ ids: '1,2' }) .expect(404); }); @@ -2540,7 +2538,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authMemberAgent .delete(`/projects/${ownerProject.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '1' }) .expect(403); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); @@ -2571,7 +2568,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authMemberAgent .delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '1' }) .expect(403); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); @@ -2611,7 +2607,15 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authMemberAgent .delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '1,3' }) + .send({ + filter: { + type: 'or', + filters: [ + { columnName: 'first', condition: 'eq', value: 'test value 1' }, + { columnName: 'first', condition: 'eq', value: 'test value 3' }, + ], + }, + }) .expect(200); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); @@ -2651,7 +2655,12 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authAdminAgent .delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '2' }) + .send({ + filter: { + type: 'and', + filters: [{ columnName: 'first', condition: 'eq', value: 'test value 2' }], + }, + }) .expect(200); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); @@ -2690,11 +2699,17 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authOwnerAgent .delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '1,2' }) + .send({ + filter: { + type: 'and', + filters: [{ columnName: 'first', condition: 'eq', value: 'test value 2' }], + }, + }) .expect(200); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); - expect(rowsInDb.count).toBe(0); + expect(rowsInDb.count).toBe(1); + expect(rowsInDb.data.map((r) => r.first)).toEqual(['test value 1']); }); test('should delete rows in personal project', async () => { @@ -2727,7 +2742,12 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { await authMemberAgent .delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '2' }) + .send({ + filter: { + type: 'and', + filters: [{ columnName: 'first', condition: 'eq', value: 'test value 2' }], + }, + }) .expect(200); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); @@ -2735,81 +2755,53 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => { expect(rowsInDb.data.map((r) => r.first).sort()).toEqual(['test value 1', 'test value 3']); }); - test('should return true when deleting empty ids array', async () => { + test('should return full deleted data if returnData is set', async () => { const dataStore = await createDataStore(memberProject, { columns: [ { name: 'first', type: 'string', }, - ], - data: [ { - first: 'test value', - }, - ], - }); - - const response = await authMemberAgent - .delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '' }) - .expect(200); - - expect(response.body.data).toBe(true); - - const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); - expect(rowsInDb.count).toBe(1); - }); - - test('should handle deletion of non-existing row ids gracefully', async () => { - const dataStore = await createDataStore(memberProject, { - columns: [ - { - name: 'first', - type: 'string', - }, - ], - data: [ - { - first: 'test value', - }, - ], - }); - - await authMemberAgent - .delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '999,1000' }) - .expect(200); - - const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); - expect(rowsInDb.count).toBe(1); - }); - - test('should handle mixed existing and non-existing row ids', async () => { - const dataStore = await createDataStore(memberProject, { - columns: [ - { - name: 'first', + name: 'second', type: 'string', }, ], data: [ { first: 'test value 1', + second: 'another value 1', }, { first: 'test value 2', + second: 'another value 2', + }, + { + first: 'test value 3', + second: 'another value 3', }, ], }); - await authMemberAgent + const result = await authMemberAgent .delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`) - .query({ ids: '1,999,2,1000' }) - .expect(200); + .send({ + filter: { + type: 'and', + filters: [{ columnName: 'first', condition: 'eq', value: 'test value 3' }], + }, + returnData: true, + }); - const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); - expect(rowsInDb.count).toBe(0); + expect(result.body.data).toEqual([ + { + id: expect.any(Number), + first: 'test value 3', + second: 'another value 3', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + ]); }); }); diff --git a/packages/cli/src/modules/data-table/__tests__/data-store.service.test.ts b/packages/cli/src/modules/data-table/__tests__/data-store.service.test.ts index 9daecbad18..e3f0d8f6db 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-store.service.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-store.service.test.ts @@ -938,7 +938,12 @@ describe('dataStore', () => { ); expect(ids).toEqual([{ id: 1 }, { id: 2 }]); - await dataStoreService.deleteRows(dataStoreId, project1.id, [ids[0].id]); + await dataStoreService.deleteRows(dataStoreId, project1.id, { + filter: { + type: 'and', + filters: [{ columnName: 'id', condition: 'eq', value: ids[0].id }], + }, + }); // Insert a new row const result = await dataStoreService.insertRows( @@ -1596,7 +1601,53 @@ describe('dataStore', () => { }); describe('deleteRows', () => { - it('deletes rows by IDs', async () => { + it('should delete rows by filter condition', async () => { + // ARRANGE + const dataStore = await dataStoreService.createDataStore(project1.id, { + name: 'dataStore', + columns: [ + { name: 'name', type: 'string' }, + { name: 'age', type: 'number' }, + { name: 'active', type: 'boolean' }, + ], + }); + const { id: dataStoreId } = dataStore; + + // Insert test rows + await dataStoreService.insertRows( + dataStoreId, + project1.id, + [ + { name: 'Alice', age: 30, active: true }, + { name: 'Bob', age: 25, active: false }, + { name: 'Charlie', age: 35, active: true }, + ], + 'id', + ); + + // ACT + const result = await dataStoreService.deleteRows(dataStoreId, project1.id, { + filter: { + type: 'and', + filters: [{ columnName: 'active', condition: 'eq', value: true }], + }, + }); + + // ASSERT + expect(result).toBe(true); + + const rows = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {}); + expect(rows.count).toBe(1); + expect(rows.data).toEqual([ + expect.objectContaining({ + name: 'Bob', + age: 25, + active: false, + }), + ]); + }); + + it('should delete rows by ids', async () => { // ARRANGE const dataStore = await dataStoreService.createDataStore(project1.id, { name: 'dataStore', @@ -1608,7 +1659,7 @@ describe('dataStore', () => { const { id: dataStoreId } = dataStore; // Insert test rows - const ids = await dataStoreService.insertRows( + const insertedIds = await dataStoreService.insertRows( dataStoreId, project1.id, [ @@ -1618,10 +1669,59 @@ describe('dataStore', () => { ], 'id', ); - expect(ids).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); - // ACT - Delete first and third rows - const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 3]); + const filters = insertedIds + .slice(1) + .map(({ id }) => ({ columnName: 'id', condition: 'eq' as const, value: id })); + + // ACT + const result = await dataStoreService.deleteRows(dataStoreId, project1.id, { + filter: { + type: 'or', + filters, + }, + }); + + // ASSERT + expect(result).toBe(true); + + const rows = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {}); + expect(rows.count).toBe(1); + expect(rows.data).toEqual([ + expect.objectContaining({ + id: insertedIds[0].id, + }), + ]); + }); + + it('should delete only one row with OR filter', async () => { + // ARRANGE + const dataStore = await dataStoreService.createDataStore(project1.id, { + name: 'dataStore', + columns: [ + { name: 'name', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }); + const { id: dataStoreId } = dataStore; + + await dataStoreService.insertRows( + dataStoreId, + project1.id, + [ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 25 }, + ], + 'id', + ); + + // ACT + const result = await dataStoreService.deleteRows(dataStoreId, project1.id, { + filter: { + type: 'or', + filters: [{ columnName: 'name', condition: 'eq', value: 'Alice' }], + }, + }); // ASSERT expect(result).toBe(true); @@ -1636,29 +1736,62 @@ describe('dataStore', () => { ]); }); - it('returns true when deleting empty list of IDs', async () => { + it('return full deleted data if returnData is set', async () => { // ARRANGE const dataStore = await dataStoreService.createDataStore(project1.id, { name: 'dataStore', - columns: [{ name: 'name', type: 'string' }], + columns: [ + { name: 'name', type: 'string' }, + { name: 'age', type: 'number' }, + ], }); const { id: dataStoreId } = dataStore; + await dataStoreService.insertRows( + dataStoreId, + project1.id, + [{ name: 'Alice', age: 30 }], + 'id', + ); + // ACT - const result = await dataStoreService.deleteRows(dataStoreId, project1.id, []); + const result = await dataStoreService.deleteRows( + dataStoreId, + project1.id, + { + filter: { + type: 'and', + filters: [{ columnName: 'name', condition: 'eq', value: 'Alice' }], + }, + }, + true, + ); // ASSERT - expect(result).toBe(true); + expect(result).toEqual([ + { + id: expect.any(Number), + name: 'Alice', + age: 30, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }, + ]); }); it('fails when trying to delete from non-existent data store', async () => { // ACT & ASSERT - const result = dataStoreService.deleteRows('non-existent-id', project1.id, [1, 2]); + const result = dataStoreService.deleteRows('non-existent-id', project1.id, { + filter: { + type: 'and', + filters: [{ columnName: 'id', condition: 'eq', value: 1 }], + }, + }); await expect(result).rejects.toThrow(DataStoreNotFoundError); }); - it('succeeds even if some IDs do not exist', async () => { + it('should return true and do nothing when no rows match the filter', async () => { // ARRANGE const dataStore = await dataStoreService.createDataStore(project1.id, { name: 'dataStore', @@ -1666,17 +1799,39 @@ describe('dataStore', () => { }); const { id: dataStoreId } = dataStore; - // Insert one row - const ids = await dataStoreService.insertRows( - dataStoreId, - project1.id, - [{ name: 'Alice' }], - 'id', - ); - expect(ids).toEqual([{ id: 1 }]); + await dataStoreService.insertRows(dataStoreId, project1.id, [{ name: 'Alice' }]); - // ACT - Try to delete existing and non-existing IDs - const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 999, 1000]); + // ACT + const result = await dataStoreService.deleteRows(dataStoreId, project1.id, { + filter: { + type: 'and', + filters: [{ columnName: 'name', condition: 'eq', value: 'Charlie' }], + }, + }); + + // ASSERT + expect(result).toBe(true); + + const { count } = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {}); + expect(count).toBe(1); + }); + + it('should delete all rows when no filter is provided', async () => { + // ARRANGE + const dataStore = await dataStoreService.createDataStore(project1.id, { + name: 'dataStore', + columns: [{ name: 'name', type: 'string' }], + }); + const { id: dataStoreId } = dataStore; + + await dataStoreService.insertRows(dataStoreId, project1.id, [ + { name: 'Alice' }, + { name: 'Bob' }, + { name: 'Charlie' }, + ]); + + // ACT + const result = await dataStoreService.deleteRows(dataStoreId, project1.id, {}); // ASSERT expect(result).toBe(true); diff --git a/packages/cli/src/modules/data-table/data-store-proxy.service.ts b/packages/cli/src/modules/data-table/data-store-proxy.service.ts index ad405794df..4ca6a5e742 100644 --- a/packages/cli/src/modules/data-table/data-store-proxy.service.ts +++ b/packages/cli/src/modules/data-table/data-store-proxy.service.ts @@ -8,6 +8,7 @@ import { DataStoreColumn, DataStoreProxyProvider, DataStoreRows, + DeleteDataTableRowsOptions, IDataStoreProjectAggregateService, IDataStoreProjectService, DataTableInsertRowsReturnType, @@ -147,8 +148,8 @@ export class DataStoreProxyService implements DataStoreProxyProvider { return await dataStoreService.upsertRow(dataStoreId, projectId, options, true); }, - async deleteRows(ids: number[]) { - return await dataStoreService.deleteRows(dataStoreId, projectId, ids); + async deleteRows(options: DeleteDataTableRowsOptions) { + return await dataStoreService.deleteRows(dataStoreId, projectId, options, true); }, }; } diff --git a/packages/cli/src/modules/data-table/data-store-rows.repository.ts b/packages/cli/src/modules/data-table/data-store-rows.repository.ts index 5c045a0140..10d51fae5b 100644 --- a/packages/cli/src/modules/data-table/data-store-rows.repository.ts +++ b/packages/cli/src/modules/data-table/data-store-rows.repository.ts @@ -9,6 +9,7 @@ import { UpdateQueryBuilder, In, ObjectLiteral, + DeleteQueryBuilder, } from '@n8n/typeorm'; import { DataStoreColumnJsType, @@ -335,21 +336,65 @@ export class DataStoreRowsRepository { return await this.getManyByIds(dataStoreId, ids, columns); } - async deleteRows(dataStoreId: string, ids: number[]) { - if (ids.length === 0) { + async deleteRows( + dataTableId: string, + columns: DataTableColumn[], + filter: DataTableFilter | undefined, + returnData: boolean = false, + ) { + const dbType = this.dataSource.options.type; + const useReturning = dbType === 'postgres'; + const table = toTableName(dataTableId); + + if (!returnData) { + // Just delete and return true + await this.dataSource.manager.transaction(async (em) => { + const query = em.createQueryBuilder().delete().from(table, 'dataTable'); + if (filter) { + this.applyFilters(query, filter, undefined, columns); + } + await query.execute(); + }); return true; } - const table = toTableName(dataStoreId); + let affectedRows: DataStoreRowReturn[] = []; - await this.dataSource - .createQueryBuilder() - .delete() - .from(table, 'dataTable') - .where({ id: In(ids) }) - .execute(); + await this.dataSource.manager.transaction(async (em) => { + if (!useReturning) { + const selectQuery = em.createQueryBuilder().select('*').from(table, 'dataTable'); - return true; + if (filter) { + this.applyFilters(selectQuery, filter, 'dataTable', columns); + } + + const rawRows = await selectQuery.getRawMany(); + affectedRows = normalizeRows(rawRows, columns); + } + + const query = em.createQueryBuilder().delete().from(table, 'dataTable'); + + if (useReturning) { + const escapedColumns = columns.map((c) => this.dataSource.driver.escape(c.name)); + const escapedSystemColumns = DATA_TABLE_SYSTEM_COLUMNS.map((x) => + this.dataSource.driver.escape(x), + ); + const selectColumns = [...escapedSystemColumns, ...escapedColumns]; + query.returning(selectColumns.join(',')); + } + + if (filter) { + this.applyFilters(query, filter, undefined, columns); + } + + const result = await query.execute(); + + if (useReturning) { + affectedRows = normalizeRows(extractReturningData(result.raw), columns); + } + }); + + return affectedRows; } async createTableWithColumns( @@ -450,7 +495,7 @@ export class DataStoreRowsRepository { } private applyFilters( - query: SelectQueryBuilder | UpdateQueryBuilder, + query: SelectQueryBuilder | UpdateQueryBuilder | DeleteQueryBuilder, filter: DataTableFilter, tableReference?: string, columns?: DataTableColumn[], @@ -463,11 +508,17 @@ export class DataStoreRowsRepository { getConditionAndParams(filter, i, dbType, tableReference, columns), ); - for (const [condition, params] of conditionsAndParams) { - if (filterType === 'or') { - query.orWhere(condition, params); - } else { - query.andWhere(condition, params); + if (conditionsAndParams.length === 1) { + // Always use AND for a single filter + const [condition, params] = conditionsAndParams[0]; + query.andWhere(condition, params); + } else { + for (const [condition, params] of conditionsAndParams) { + if (filterType === 'or') { + query.orWhere(condition, params); + } else { + query.andWhere(condition, params); + } } } } diff --git a/packages/cli/src/modules/data-table/data-store.controller.ts b/packages/cli/src/modules/data-table/data-store.controller.ts index cca386c433..09cd1bed0e 100644 --- a/packages/cli/src/modules/data-table/data-store.controller.ts +++ b/packages/cli/src/modules/data-table/data-store.controller.ts @@ -2,7 +2,7 @@ import { AddDataStoreRowsDto, AddDataStoreColumnDto, CreateDataStoreDto, - DeleteDataStoreRowsQueryDto, + DeleteDataTableRowsDto, ListDataStoreContentQueryDto, ListDataStoreQueryDto, MoveDataStoreColumnDto, @@ -328,20 +328,26 @@ export class DataStoreController { } } - @Delete('/:dataStoreId/rows') + @Delete('/:dataTableId/rows') @ProjectScope('dataStore:writeRow') - async deleteDataStoreRows( + async deleteDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, - @Param('dataStoreId') dataStoreId: string, - @Query dto: DeleteDataStoreRowsQueryDto, + @Param('dataTableId') dataTableId: string, + @Body dto: DeleteDataTableRowsDto, ) { try { - const { ids } = dto; - return await this.dataStoreService.deleteRows(dataStoreId, req.params.projectId, ids); + return await this.dataStoreService.deleteRows( + dataTableId, + req.params.projectId, + dto, + dto.returnData, + ); } catch (e: unknown) { if (e instanceof DataStoreNotFoundError) { throw new NotFoundError(e.message); + } else if (e instanceof DataStoreValidationError) { + throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { diff --git a/packages/cli/src/modules/data-table/data-store.service.ts b/packages/cli/src/modules/data-table/data-store.service.ts index 6abbb59ebd..bfdaab71b2 100644 --- a/packages/cli/src/modules/data-table/data-store.service.ts +++ b/packages/cli/src/modules/data-table/data-store.service.ts @@ -1,6 +1,7 @@ import type { AddDataStoreColumnDto, CreateDataStoreDto, + DeleteDataTableRowsDto, ListDataStoreContentQueryDto, MoveDataStoreColumnDto, DataStoreListOptions, @@ -229,10 +230,38 @@ export class DataStoreService { ); } - async deleteRows(dataStoreId: string, projectId: string, ids: number[]) { + async deleteRows( + dataStoreId: string, + projectId: string, + dto: Omit, + returnData?: T, + ): Promise; + async deleteRows( + dataStoreId: string, + projectId: string, + dto: Omit, + returnData: boolean = false, + ) { await this.validateDataStoreExists(dataStoreId, projectId); - return await this.dataStoreRowsRepository.deleteRows(dataStoreId, ids); + const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId); + if (columns.length === 0) { + throw new DataStoreValidationError( + 'No columns found for this data table or data table not found', + ); + } + + if (dto.filter?.filters && dto.filter.filters.length !== 0) { + this.validateAndTransformFilters(dto.filter, columns); + } + + const result = await this.dataStoreRowsRepository.deleteRows( + dataStoreId, + columns, + dto.filter, + returnData, + ); + return returnData ? result : true; } private validateRowsWithColumns( diff --git a/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.test.ts b/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.test.ts index f4b40cd372..eaa5507954 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.test.ts +++ b/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.test.ts @@ -31,7 +31,14 @@ describe('dataStore.api', () => { 'DELETE', `/projects/${projectId}/data-tables/${dataStoreId}/rows`, { - ids: '1,2,3', + filter: { + type: 'or', + filters: [ + { columnName: 'id', condition: 'eq', value: 1 }, + { columnName: 'id', condition: 'eq', value: 2 }, + { columnName: 'id', condition: 'eq', value: 3 }, + ], + }, }, ); expect(result).toBe(true); diff --git a/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.ts b/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.ts index 1ccf9860fb..056274c05a 100644 --- a/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.ts +++ b/packages/frontend/editor-ui/src/features/dataStore/dataStore.api.ts @@ -190,12 +190,16 @@ export const deleteDataStoreRowsApi = async ( rowIds: number[], projectId: string, ) => { + const filters = rowIds.map((id) => ({ columnName: 'id', condition: 'eq', value: id })); return await makeRestApiRequest( context, 'DELETE', `/projects/${projectId}/data-tables/${dataStoreId}/rows`, { - ids: rowIds.join(','), + filter: { + type: 'or', + filters, + }, }, ); }; diff --git a/packages/nodes-base/nodes/DataTable/actions/row/delete.operation.ts b/packages/nodes-base/nodes/DataTable/actions/row/delete.operation.ts index 9d42224722..9d9d464c4c 100644 --- a/packages/nodes-base/nodes/DataTable/actions/row/delete.operation.ts +++ b/packages/nodes-base/nodes/DataTable/actions/row/delete.operation.ts @@ -7,7 +7,7 @@ import { } from 'n8n-workflow'; import { DRY_RUN } from '../../common/fields'; -import { executeSelectMany, getSelectFields } from '../../common/selectMany'; +import { getSelectFields, getSelectFilter } from '../../common/selectMany'; import { getDataTableProxyExecute } from '../../common/utils'; // named `deleteRows` since `delete` is a reserved keyword @@ -48,14 +48,14 @@ export async function execute( ); } - const matches = await executeSelectMany(this, index, dataStoreProxy); + const filter = getSelectFilter(this, index); - if (!dryRun) { - const success = await dataStoreProxy.deleteRows(matches.map((x) => x.json.id)); - if (!success) { - throw new NodeOperationError(this.getNode(), `failed to delete rows for index ${index}`); - } + if (dryRun) { + const { data: rowsToDelete } = await dataStoreProxy.getManyRowsAndCount({ filter }); + return rowsToDelete.map((json) => ({ json })); } - return matches; + const result = await dataStoreProxy.deleteRows({ filter }); + + return result.map((json) => ({ json })); } diff --git a/packages/nodes-base/nodes/DataTable/common/selectMany.ts b/packages/nodes-base/nodes/DataTable/common/selectMany.ts index 09a5a6d09f..d208172161 100644 --- a/packages/nodes-base/nodes/DataTable/common/selectMany.ts +++ b/packages/nodes-base/nodes/DataTable/common/selectMany.ts @@ -125,7 +125,7 @@ export async function executeSelectMany( const PAGE_SIZE = 1000; const result: Array<{ json: DataStoreRowReturn }> = []; - const limit = ctx.getNodeParameter('limit', index, undefined); + const limit = ctx.getNodeParameter('limit', index, 0); let expectedTotal: number | undefined; let skip = 0; diff --git a/packages/workflow/src/data-store.types.ts b/packages/workflow/src/data-store.types.ts index 80fa618f8f..4938c8b077 100644 --- a/packages/workflow/src/data-store.types.ts +++ b/packages/workflow/src/data-store.types.ts @@ -67,6 +67,10 @@ export type UpsertDataStoreRowOptions = { data: DataStoreRow; }; +export type DeleteDataTableRowsOptions = { + filter?: DataTableFilter; +}; + export type MoveDataStoreColumnOptions = { targetIndex: number; }; @@ -141,5 +145,5 @@ export interface IDataStoreProjectService { upsertRow(options: UpsertDataStoreRowOptions): Promise; - deleteRows(ids: number[]): Promise; + deleteRows(options: DeleteDataTableRowsOptions): Promise; }