mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Use filters for delete data table rows (no-changelog) (#19375)
This commit is contained in:
@@ -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',
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
@@ -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) {}
|
||||||
@@ -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';
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +508,11 @@ export class DataStoreRowsRepository {
|
|||||||
getConditionAndParams(filter, i, dbType, tableReference, columns),
|
getConditionAndParams(filter, i, dbType, tableReference, columns),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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) {
|
for (const [condition, params] of conditionsAndParams) {
|
||||||
if (filterType === 'or') {
|
if (filterType === 'or') {
|
||||||
query.orWhere(condition, params);
|
query.orWhere(condition, params);
|
||||||
@@ -471,6 +521,7 @@ export class DataStoreRowsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applySorting(query: QueryBuilder, dto: ListDataStoreContentQueryDto): void {
|
private applySorting(query: QueryBuilder, dto: ListDataStoreContentQueryDto): void {
|
||||||
if (!dto.sortBy) {
|
if (!dto.sortBy) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user