mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add delete rows endpoint to data store module (no-changelog) (#18376)
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
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',
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
@@ -90,3 +90,4 @@ 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';
|
||||||
|
|||||||
@@ -1942,6 +1942,318 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
|
||||||
|
test('should not delete rows when project does not exist', async () => {
|
||||||
|
await authOwnerAgent
|
||||||
|
.delete('/projects/non-existing-id/data-stores/some-data-store-id/rows')
|
||||||
|
.query({ ids: '1,2' })
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not delete rows when data store does not exist', async () => {
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
|
||||||
|
await authOwnerAgent
|
||||||
|
.delete(`/projects/${project.id}/data-stores/non-existing-id/rows`)
|
||||||
|
.query({ ids: '1,2' })
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not delete rows in another user's personal project data store", async () => {
|
||||||
|
const dataStore = await createDataStore(ownerProject, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value',
|
||||||
|
second: 'another value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await authMemberAgent
|
||||||
|
.delete(`/projects/${ownerProject.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '1' })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not delete rows if user has project:viewer role in team project', async () => {
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
await linkUserToProject(member, project, 'project:viewer');
|
||||||
|
const dataStore = await createDataStore(project, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value',
|
||||||
|
second: 'another value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await authMemberAgent
|
||||||
|
.delete(`/projects/${project.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '1' })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete rows if user has project:editor role in team project', async () => {
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
await linkUserToProject(member, project, 'project:editor');
|
||||||
|
|
||||||
|
const dataStore = await createDataStore(project, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
.delete(`/projects/${project.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '1,3' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(1);
|
||||||
|
expect(rowsInDb.data[0]).toMatchObject({
|
||||||
|
first: 'test value 2',
|
||||||
|
second: 'another value 2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete rows if user has project:admin role in team project', async () => {
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
await linkUserToProject(admin, project, 'project:admin');
|
||||||
|
|
||||||
|
const dataStore = await createDataStore(project, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value 1',
|
||||||
|
second: 'another value 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'test value 2',
|
||||||
|
second: 'another value 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await authAdminAgent
|
||||||
|
.delete(`/projects/${project.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '2' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(1);
|
||||||
|
expect(rowsInDb.data[0]).toMatchObject({
|
||||||
|
first: 'test value 1',
|
||||||
|
second: 'another value 1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete rows if user is owner in team project', async () => {
|
||||||
|
const project = await createTeamProject('test project', owner);
|
||||||
|
|
||||||
|
const dataStore = await createDataStore(project, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'second',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value 1',
|
||||||
|
second: 'another value 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'test value 2',
|
||||||
|
second: 'another value 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await authOwnerAgent
|
||||||
|
.delete(`/projects/${project.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '1,2' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete rows in personal project', async () => {
|
||||||
|
const dataStore = await createDataStore(memberProject, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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
|
||||||
|
.delete(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '2' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(2);
|
||||||
|
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 () => {
|
||||||
|
const dataStore = await createDataStore(memberProject, {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'first',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await authMemberAgent
|
||||||
|
.delete(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(response.body.data).toBe(true);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(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-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '999,1000' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
first: 'test value 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
first: 'test value 2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await authMemberAgent
|
||||||
|
.delete(`/projects/${memberProject.id}/data-stores/${dataStore.id}/rows`)
|
||||||
|
.query({ ids: '1,999,2,1000' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {});
|
||||||
|
expect(rowsInDb.count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
|
describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
|
||||||
test('should not upsert rows when project does not exist', async () => {
|
test('should not upsert rows when project does not exist', async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -1131,6 +1131,84 @@ describe('dataStore', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deleteRows', () => {
|
||||||
|
it('deletes rows by IDs', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||||
|
name: 'dataStore',
|
||||||
|
columns: [
|
||||||
|
{ name: 'name', type: 'string' },
|
||||||
|
{ name: 'age', type: 'number' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { id: dataStoreId } = dataStore;
|
||||||
|
|
||||||
|
// Insert test rows
|
||||||
|
await dataStoreService.insertRows(dataStoreId, project1.id, [
|
||||||
|
{ name: 'Alice', age: 30 },
|
||||||
|
{ name: 'Bob', age: 25 },
|
||||||
|
{ name: 'Charlie', age: 35 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get initial data to find row IDs
|
||||||
|
const initialData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
|
expect(initialData.count).toBe(3);
|
||||||
|
|
||||||
|
// ACT - Delete first and third rows
|
||||||
|
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, [1, 3]);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const rows = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
|
expect(rows.count).toBe(1);
|
||||||
|
expect(rows.data).toEqual([{ name: 'Bob', age: 25, id: 2 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when deleting empty list of IDs', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||||
|
name: 'dataStore',
|
||||||
|
columns: [{ name: 'name', type: 'string' }],
|
||||||
|
});
|
||||||
|
const { id: dataStoreId } = dataStore;
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
const result = await dataStoreService.deleteRows(dataStoreId, project1.id, []);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
await expect(result).rejects.toThrow(DataStoreNotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('succeeds even if some IDs do not exist', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const dataStore = await dataStoreService.createDataStore(project1.id, {
|
||||||
|
name: 'dataStore',
|
||||||
|
columns: [{ name: 'name', type: 'string' }],
|
||||||
|
});
|
||||||
|
const { id: dataStoreId } = dataStore;
|
||||||
|
|
||||||
|
// Insert one row
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const { count } = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {});
|
||||||
|
expect(count).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getManyRowsAndCount', () => {
|
describe('getManyRowsAndCount', () => {
|
||||||
it('retrieves rows correctly', async () => {
|
it('retrieves rows correctly', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
|
|||||||
@@ -98,6 +98,20 @@ export class DataStoreRowsRepository {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteRows(tableName: DataStoreUserTableName, ids: number[]) {
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbType = this.dataSource.options.type;
|
||||||
|
const quotedTableName = quoteIdentifier(tableName, dbType);
|
||||||
|
const placeholders = ids.map((_, index) => getPlaceholder(index + 1, dbType)).join(', ');
|
||||||
|
const query = `DELETE FROM ${quotedTableName} WHERE id IN (${placeholders})`;
|
||||||
|
|
||||||
|
await this.dataSource.query(query, ids);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async createTableWithColumns(
|
async createTableWithColumns(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columns: DataStoreColumn[],
|
columns: DataStoreColumn[],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AddDataStoreRowsDto,
|
AddDataStoreRowsDto,
|
||||||
AddDataStoreColumnDto,
|
AddDataStoreColumnDto,
|
||||||
CreateDataStoreDto,
|
CreateDataStoreDto,
|
||||||
|
DeleteDataStoreRowsQueryDto,
|
||||||
ListDataStoreContentQueryDto,
|
ListDataStoreContentQueryDto,
|
||||||
ListDataStoreQueryDto,
|
ListDataStoreQueryDto,
|
||||||
MoveDataStoreColumnDto,
|
MoveDataStoreColumnDto,
|
||||||
@@ -277,4 +278,26 @@ export class DataStoreController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('/:dataStoreId/rows')
|
||||||
|
@ProjectScope('dataStore:writeRow')
|
||||||
|
async deleteDataStoreRows(
|
||||||
|
req: AuthenticatedRequest<{ projectId: string }>,
|
||||||
|
_res: Response,
|
||||||
|
@Param('dataStoreId') dataStoreId: string,
|
||||||
|
@Query dto: DeleteDataStoreRowsQueryDto,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { ids } = dto;
|
||||||
|
return await this.dataStoreService.deleteRows(dataStoreId, req.params.projectId, ids);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof DataStoreNotFoundError) {
|
||||||
|
throw new NotFoundError(e.message);
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
throw new InternalServerError(e.message, e);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ export class DataStoreService {
|
|||||||
return await this.dataStoreRowsRepository.upsertRows(toTableName(dataStoreId), dto, columns);
|
return await this.dataStoreRowsRepository.upsertRows(toTableName(dataStoreId), dto, columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteRows(dataStoreId: string, projectId: string, ids: number[]) {
|
||||||
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
||||||
|
|
||||||
|
return await this.dataStoreRowsRepository.deleteRows(toTableName(dataStoreId), ids);
|
||||||
|
}
|
||||||
|
|
||||||
private async validateRows(dataStoreId: string, rows: DataStoreRows): Promise<void> {
|
private async validateRows(dataStoreId: string, rows: DataStoreRows): Promise<void> {
|
||||||
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
||||||
if (columns.length === 0) {
|
if (columns.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user