From 56b3a0e8a5d45b9c0cea9cfe1782236f2b80297b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:14:32 +0100 Subject: [PATCH] Fix REST API Delete and Destroy (#15806) Fixes https://github.com/twentyhq/twenty/issues/15801 --- .../handlers/rest-api-delete-many.handler.ts | 12 +++++- .../handlers/rest-api-destroy-many.handler.ts | 12 +++++- .../rest-api-core-delete.integration-spec.ts | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts index d14b3d9ef8..85e19dd1c2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-delete-many.handler.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonDeleteManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -23,6 +24,13 @@ export class RestApiDeleteManyHandler extends RestApiBaseHandler { }; }> { const { filter } = this.parseRequestArgs(request); + + if (isEmpty(filter)) { + throw new BadRequestException( + 'Filters are mandatory for bulk delete operations. Please provide at least one filter to prevent accidental deletion of all records.', + ); + } + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = await this.buildCommonOptions(request); diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts index 709d2b940e..3f16703d62 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-destroy-many.handler.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import isEmpty from 'lodash.isempty'; import { ObjectRecord } from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { CommonDestroyManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-destroy-many-query-runner.service'; +import { RestApiBaseHandler } from 'src/engine/api/rest/core/handlers/rest-api-base.handler'; import { parseFilterRestRequest } from 'src/engine/api/rest/input-request-parsers/filter-parser-utils/parse-filter-rest-request.util'; import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request'; import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util'; @@ -23,6 +24,13 @@ export class RestApiDestroyManyHandler extends RestApiBaseHandler { }; }> { const { filter } = this.parseRequestArgs(request); + + if (isEmpty(filter)) { + throw new BadRequestException( + 'Filters are mandatory for bulk destroy operations. Please provide at least one filter to prevent accidental deletion of all records.', + ); + } + const { authContext, objectMetadataItemWithFieldMaps, objectMetadataMaps } = await this.buildCommonOptions(request); diff --git a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts index 7d64c27cd7..103284aa14 100644 --- a/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/rest-api-core-delete.integration-spec.ts @@ -61,3 +61,41 @@ describe('Core REST API Delete One endpoint', () => { expect(response.body.messages[0]).toBe('Record not found'); }); }); + +describe('Core REST API Delete Many endpoint', () => { + beforeAll(async () => { + await deleteAllRecords('person'); + }); + + it('should require filters for bulk delete operations', async () => { + const response = await makeRestAPIRequest({ + method: 'delete', + path: `/people?soft_delete=true`, + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('BadRequestException'); + expect(response.body.messages[0]).toContain( + 'Filters are mandatory for bulk delete operations', + ); + }); +}); + +describe('Core REST API Destroy Many endpoint', () => { + beforeAll(async () => { + await deleteAllRecords('person'); + }); + + it('should require filters for bulk destroy operations', async () => { + const response = await makeRestAPIRequest({ + method: 'delete', + path: `/people?soft_delete=false`, + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('BadRequestException'); + expect(response.body.messages[0]).toContain( + 'Filters are mandatory for bulk destroy operations', + ); + }); +});