feat(core): Use filters for delete data table rows (no-changelog) (#19375)

This commit is contained in:
Daria
2025-09-11 10:31:27 +03:00
committed by GitHub
parent 6dd7797c39
commit b147709189
14 changed files with 384 additions and 138 deletions

View File

@@ -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',
}),
}) {}

View File

@@ -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) {}

View File

@@ -86,6 +86,7 @@ export { OidcConfigDto } from './oidc/config.dto';
export { CreateDataStoreDto } from './data-store/create-data-store.dto'; export { CreateDataStoreDto } from './data-store/create-data-store.dto';
export { UpdateDataStoreDto } from './data-store/update-data-store.dto'; export { UpdateDataStoreDto } from './data-store/update-data-store.dto';
export { UpdateDataTableRowDto } from './data-store/update-data-store-row.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 { UpsertDataStoreRowDto } from './data-store/upsert-data-store-row.dto';
export { ListDataStoreQueryDto } from './data-store/list-data-store-query.dto'; export { ListDataStoreQueryDto } from './data-store/list-data-store-query.dto';
export { ListDataStoreContentQueryDto } from './data-store/list-data-store-content-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 { AddDataStoreRowsDto } from './data-store/add-data-store-rows.dto';
export { AddDataStoreColumnDto } from './data-store/add-data-store-column.dto'; export { AddDataStoreColumnDto } from './data-store/add-data-store-column.dto';
export { MoveDataStoreColumnDto } from './data-store/move-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';

View File

@@ -2505,7 +2505,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
test('should not delete rows when project does not exist', async () => { test('should not delete rows when project does not exist', async () => {
await authOwnerAgent await authOwnerAgent
.delete('/projects/non-existing-id/data-tables/some-data-store-id/rows') .delete('/projects/non-existing-id/data-tables/some-data-store-id/rows')
.query({ ids: '1,2' })
.expect(403); .expect(403);
}); });
@@ -2514,7 +2513,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authOwnerAgent await authOwnerAgent
.delete(`/projects/${project.id}/data-tables/non-existing-id/rows`) .delete(`/projects/${project.id}/data-tables/non-existing-id/rows`)
.query({ ids: '1,2' })
.expect(404); .expect(404);
}); });
@@ -2540,7 +2538,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authMemberAgent await authMemberAgent
.delete(`/projects/${ownerProject.id}/data-tables/${dataStore.id}/rows`) .delete(`/projects/${ownerProject.id}/data-tables/${dataStore.id}/rows`)
.query({ ids: '1' })
.expect(403); .expect(403);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
@@ -2571,7 +2568,6 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authMemberAgent await authMemberAgent
.delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) .delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`)
.query({ ids: '1' })
.expect(403); .expect(403);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
@@ -2611,7 +2607,15 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authMemberAgent await authMemberAgent
.delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) .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); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
@@ -2651,7 +2655,12 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authAdminAgent await authAdminAgent
.delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) .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); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
@@ -2690,11 +2699,17 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authOwnerAgent await authOwnerAgent
.delete(`/projects/${project.id}/data-tables/${dataStore.id}/rows`) .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); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); 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 () => { test('should delete rows in personal project', async () => {
@@ -2727,7 +2742,12 @@ describe('DELETE /projects/:projectId/data-tables/:dataStoreId/rows', () => {
await authMemberAgent await authMemberAgent
.delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`) .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); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); 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']); 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, { const dataStore = await createDataStore(memberProject, {
columns: [ columns: [
{ {
name: 'first', name: 'first',
type: 'string', type: 'string',
}, },
],
data: [
{ {
first: 'test value', name: 'second',
},
],
});
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',
type: 'string', type: 'string',
}, },
], ],
data: [ data: [
{ {
first: 'test value 1', first: 'test value 1',
second: 'another value 1',
}, },
{ {
first: 'test value 2', 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`) .delete(`/projects/${memberProject.id}/data-tables/${dataStore.id}/rows`)
.query({ ids: '1,999,2,1000' }) .send({
.expect(200); filter: {
type: 'and',
filters: [{ columnName: 'first', condition: 'eq', value: 'test value 3' }],
},
returnData: true,
});
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {}); expect(result.body.data).toEqual([
expect(rowsInDb.count).toBe(0); {
id: expect.any(Number),
first: 'test value 3',
second: 'another value 3',
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
]);
}); });
}); });

View File

@@ -938,7 +938,12 @@ describe('dataStore', () => {
); );
expect(ids).toEqual([{ id: 1 }, { id: 2 }]); 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 // Insert a new row
const result = await dataStoreService.insertRows( const result = await dataStoreService.insertRows(
@@ -1596,7 +1601,53 @@ describe('dataStore', () => {
}); });
describe('deleteRows', () => { 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 // ARRANGE
const dataStore = await dataStoreService.createDataStore(project1.id, { const dataStore = await dataStoreService.createDataStore(project1.id, {
name: 'dataStore', name: 'dataStore',
@@ -1608,7 +1659,7 @@ describe('dataStore', () => {
const { id: dataStoreId } = dataStore; const { id: dataStoreId } = dataStore;
// Insert test rows // Insert test rows
const ids = await dataStoreService.insertRows( const insertedIds = await dataStoreService.insertRows(
dataStoreId, dataStoreId,
project1.id, project1.id,
[ [
@@ -1618,10 +1669,59 @@ describe('dataStore', () => {
], ],
'id', 'id',
); );
expect(ids).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
// ACT - Delete first and third rows const filters = insertedIds
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 3]); .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 // ASSERT
expect(result).toBe(true); 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 // ARRANGE
const dataStore = await dataStoreService.createDataStore(project1.id, { const dataStore = await dataStoreService.createDataStore(project1.id, {
name: 'dataStore', name: 'dataStore',
columns: [{ name: 'name', type: 'string' }], columns: [
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
],
}); });
const { id: dataStoreId } = dataStore; const { id: dataStoreId } = dataStore;
await dataStoreService.insertRows(
dataStoreId,
project1.id,
[{ name: 'Alice', age: 30 }],
'id',
);
// ACT // 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 // 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 () => { it('fails when trying to delete from non-existent data store', async () => {
// ACT & ASSERT // 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); 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 // ARRANGE
const dataStore = await dataStoreService.createDataStore(project1.id, { const dataStore = await dataStoreService.createDataStore(project1.id, {
name: 'dataStore', name: 'dataStore',
@@ -1666,17 +1799,39 @@ describe('dataStore', () => {
}); });
const { id: dataStoreId } = dataStore; const { id: dataStoreId } = dataStore;
// Insert one row await dataStoreService.insertRows(dataStoreId, project1.id, [{ name: 'Alice' }]);
const ids = await dataStoreService.insertRows(
dataStoreId,
project1.id,
[{ name: 'Alice' }],
'id',
);
expect(ids).toEqual([{ id: 1 }]);
// ACT - Try to delete existing and non-existing IDs // ACT
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 999, 1000]); 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 // ASSERT
expect(result).toBe(true); expect(result).toBe(true);

View File

@@ -8,6 +8,7 @@ import {
DataStoreColumn, DataStoreColumn,
DataStoreProxyProvider, DataStoreProxyProvider,
DataStoreRows, DataStoreRows,
DeleteDataTableRowsOptions,
IDataStoreProjectAggregateService, IDataStoreProjectAggregateService,
IDataStoreProjectService, IDataStoreProjectService,
DataTableInsertRowsReturnType, DataTableInsertRowsReturnType,
@@ -147,8 +148,8 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
return await dataStoreService.upsertRow(dataStoreId, projectId, options, true); return await dataStoreService.upsertRow(dataStoreId, projectId, options, true);
}, },
async deleteRows(ids: number[]) { async deleteRows(options: DeleteDataTableRowsOptions) {
return await dataStoreService.deleteRows(dataStoreId, projectId, ids); return await dataStoreService.deleteRows(dataStoreId, projectId, options, true);
}, },
}; };
} }

View File

@@ -9,6 +9,7 @@ import {
UpdateQueryBuilder, UpdateQueryBuilder,
In, In,
ObjectLiteral, ObjectLiteral,
DeleteQueryBuilder,
} from '@n8n/typeorm'; } from '@n8n/typeorm';
import { import {
DataStoreColumnJsType, DataStoreColumnJsType,
@@ -335,21 +336,65 @@ export class DataStoreRowsRepository {
return await this.getManyByIds(dataStoreId, ids, columns); return await this.getManyByIds(dataStoreId, ids, columns);
} }
async deleteRows(dataStoreId: string, ids: number[]) { async deleteRows(
if (ids.length === 0) { 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; return true;
} }
const table = toTableName(dataStoreId); let affectedRows: DataStoreRowReturn[] = [];
await this.dataSource await this.dataSource.manager.transaction(async (em) => {
.createQueryBuilder() if (!useReturning) {
.delete() const selectQuery = em.createQueryBuilder().select('*').from(table, 'dataTable');
.from(table, 'dataTable')
.where({ id: In(ids) })
.execute();
return true; if (filter) {
this.applyFilters(selectQuery, filter, 'dataTable', columns);
}
const rawRows = await selectQuery.getRawMany<DataStoreRowReturn>();
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( async createTableWithColumns(
@@ -450,7 +495,7 @@ export class DataStoreRowsRepository {
} }
private applyFilters<T extends ObjectLiteral>( private applyFilters<T extends ObjectLiteral>(
query: SelectQueryBuilder<T> | UpdateQueryBuilder<T>, query: SelectQueryBuilder<T> | UpdateQueryBuilder<T> | DeleteQueryBuilder<T>,
filter: DataTableFilter, filter: DataTableFilter,
tableReference?: string, tableReference?: string,
columns?: DataTableColumn[], columns?: DataTableColumn[],
@@ -463,11 +508,17 @@ export class DataStoreRowsRepository {
getConditionAndParams(filter, i, dbType, tableReference, columns), getConditionAndParams(filter, i, dbType, tableReference, columns),
); );
for (const [condition, params] of conditionsAndParams) { if (conditionsAndParams.length === 1) {
if (filterType === 'or') { // Always use AND for a single filter
query.orWhere(condition, params); const [condition, params] = conditionsAndParams[0];
} else { query.andWhere(condition, params);
query.andWhere(condition, params); } else {
for (const [condition, params] of conditionsAndParams) {
if (filterType === 'or') {
query.orWhere(condition, params);
} else {
query.andWhere(condition, params);
}
} }
} }
} }

View File

@@ -2,7 +2,7 @@ import {
AddDataStoreRowsDto, AddDataStoreRowsDto,
AddDataStoreColumnDto, AddDataStoreColumnDto,
CreateDataStoreDto, CreateDataStoreDto,
DeleteDataStoreRowsQueryDto, DeleteDataTableRowsDto,
ListDataStoreContentQueryDto, ListDataStoreContentQueryDto,
ListDataStoreQueryDto, ListDataStoreQueryDto,
MoveDataStoreColumnDto, MoveDataStoreColumnDto,
@@ -328,20 +328,26 @@ export class DataStoreController {
} }
} }
@Delete('/:dataStoreId/rows') @Delete('/:dataTableId/rows')
@ProjectScope('dataStore:writeRow') @ProjectScope('dataStore:writeRow')
async deleteDataStoreRows( async deleteDataTableRows(
req: AuthenticatedRequest<{ projectId: string }>, req: AuthenticatedRequest<{ projectId: string }>,
_res: Response, _res: Response,
@Param('dataStoreId') dataStoreId: string, @Param('dataTableId') dataTableId: string,
@Query dto: DeleteDataStoreRowsQueryDto, @Body dto: DeleteDataTableRowsDto,
) { ) {
try { try {
const { ids } = dto; return await this.dataStoreService.deleteRows(
return await this.dataStoreService.deleteRows(dataStoreId, req.params.projectId, ids); dataTableId,
req.params.projectId,
dto,
dto.returnData,
);
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) { if (e instanceof DataStoreNotFoundError) {
throw new NotFoundError(e.message); throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) { } else if (e instanceof Error) {
throw new InternalServerError(e.message, e); throw new InternalServerError(e.message, e);
} else { } else {

View File

@@ -1,6 +1,7 @@
import type { import type {
AddDataStoreColumnDto, AddDataStoreColumnDto,
CreateDataStoreDto, CreateDataStoreDto,
DeleteDataTableRowsDto,
ListDataStoreContentQueryDto, ListDataStoreContentQueryDto,
MoveDataStoreColumnDto, MoveDataStoreColumnDto,
DataStoreListOptions, DataStoreListOptions,
@@ -229,10 +230,38 @@ export class DataStoreService {
); );
} }
async deleteRows(dataStoreId: string, projectId: string, ids: number[]) { async deleteRows<T extends boolean | undefined>(
dataStoreId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData'>,
returnData?: T,
): Promise<T extends true ? DataStoreRowReturn[] : true>;
async deleteRows(
dataStoreId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData'>,
returnData: boolean = false,
) {
await this.validateDataStoreExists(dataStoreId, projectId); 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( private validateRowsWithColumns(

View File

@@ -31,7 +31,14 @@ describe('dataStore.api', () => {
'DELETE', 'DELETE',
`/projects/${projectId}/data-tables/${dataStoreId}/rows`, `/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); expect(result).toBe(true);

View File

@@ -190,12 +190,16 @@ export const deleteDataStoreRowsApi = async (
rowIds: number[], rowIds: number[],
projectId: string, projectId: string,
) => { ) => {
const filters = rowIds.map((id) => ({ columnName: 'id', condition: 'eq', value: id }));
return await makeRestApiRequest<boolean>( return await makeRestApiRequest<boolean>(
context, context,
'DELETE', 'DELETE',
`/projects/${projectId}/data-tables/${dataStoreId}/rows`, `/projects/${projectId}/data-tables/${dataStoreId}/rows`,
{ {
ids: rowIds.join(','), filter: {
type: 'or',
filters,
},
}, },
); );
}; };

View File

@@ -7,7 +7,7 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { DRY_RUN } from '../../common/fields'; import { DRY_RUN } from '../../common/fields';
import { executeSelectMany, getSelectFields } from '../../common/selectMany'; import { getSelectFields, getSelectFilter } from '../../common/selectMany';
import { getDataTableProxyExecute } from '../../common/utils'; import { getDataTableProxyExecute } from '../../common/utils';
// named `deleteRows` since `delete` is a reserved keyword // 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) { if (dryRun) {
const success = await dataStoreProxy.deleteRows(matches.map((x) => x.json.id)); const { data: rowsToDelete } = await dataStoreProxy.getManyRowsAndCount({ filter });
if (!success) { return rowsToDelete.map((json) => ({ json }));
throw new NodeOperationError(this.getNode(), `failed to delete rows for index ${index}`);
}
} }
return matches; const result = await dataStoreProxy.deleteRows({ filter });
return result.map((json) => ({ json }));
} }

View File

@@ -125,7 +125,7 @@ export async function executeSelectMany(
const PAGE_SIZE = 1000; const PAGE_SIZE = 1000;
const result: Array<{ json: DataStoreRowReturn }> = []; const result: Array<{ json: DataStoreRowReturn }> = [];
const limit = ctx.getNodeParameter('limit', index, undefined); const limit = ctx.getNodeParameter('limit', index, 0);
let expectedTotal: number | undefined; let expectedTotal: number | undefined;
let skip = 0; let skip = 0;

View File

@@ -67,6 +67,10 @@ export type UpsertDataStoreRowOptions = {
data: DataStoreRow; data: DataStoreRow;
}; };
export type DeleteDataTableRowsOptions = {
filter?: DataTableFilter;
};
export type MoveDataStoreColumnOptions = { export type MoveDataStoreColumnOptions = {
targetIndex: number; targetIndex: number;
}; };
@@ -141,5 +145,5 @@ export interface IDataStoreProjectService {
upsertRow(options: UpsertDataStoreRowOptions): Promise<DataStoreRowReturn[]>; upsertRow(options: UpsertDataStoreRowOptions): Promise<DataStoreRowReturn[]>;
deleteRows(ids: number[]): Promise<boolean>; deleteRows(options: DeleteDataTableRowsOptions): Promise<DataStoreRowReturn[]>;
} }