From 27f32aa2bcab235ec10b275195f750ba25e5b052 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Thu, 16 Oct 2025 23:59:49 +0530 Subject: [PATCH 1/5] fix: adds forceDelete flag to folder deletion V2 endpoint --- backend/src/lib/api-docs/constants.ts | 3 +- backend/src/server/routes/index.ts | 3 +- .../server/routes/v2/secret-folder-router.ts | 6 ++-- .../secret-folder/secret-folder-service.ts | 29 +++++++++++++++++-- .../secret-folder/secret-folder-types.ts | 1 + .../src/hooks/api/secretFolders/queries.tsx | 5 ++-- frontend/src/hooks/api/secretFolders/types.ts | 1 + 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 1799555fb..a2294a940 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -949,7 +949,8 @@ export const FOLDERS = { projectId: "The ID of the project to delete the folder from.", environment: "The slug of the environment where the folder is located.", directory: "The directory of the folder to delete. (Deprecated in favor of path)", - path: "The path of the folder to delete." + path: "The path of the folder to delete.", + forceDelete: "Whether to force delete the folder even if it contains resources." } } as const; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e95dd501c..2a3a4cf8d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1344,7 +1344,8 @@ export const registerRoutes = async ( projectDAL, folderCommitService, secretApprovalPolicyService, - secretV2BridgeDAL + secretV2BridgeDAL, + dynamicSecretDAL }); const secretImportService = secretImportServiceFactory({ diff --git a/backend/src/server/routes/v2/secret-folder-router.ts b/backend/src/server/routes/v2/secret-folder-router.ts index 0bf062452..251ad8e85 100644 --- a/backend/src/server/routes/v2/secret-folder-router.ts +++ b/backend/src/server/routes/v2/secret-folder-router.ts @@ -263,7 +263,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) // Transformations get skipped if path is undefined .transform(removeTrailingSlash) - .describe(FOLDERS.DELETE.path) + .describe(FOLDERS.DELETE.path), + forceDelete: z.boolean().optional().default(false).describe(FOLDERS.DELETE.forceDelete) }), response: { 200: z.object({ @@ -279,7 +280,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body, - idOrName: req.params.folderIdOrName + idOrName: req.params.folderIdOrName, + forceDelete: req.body.forceDelete }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 8e0892bd1..ba60b6d0d 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -5,6 +5,7 @@ import path from "path"; import { v4 as uuidv4, validate as uuidValidate } from "uuid"; import { ActionProjectType, TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas"; +import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -47,7 +48,11 @@ type TSecretFolderServiceFactoryDep = { folderCommitService: Pick; projectDAL: Pick; secretApprovalPolicyService: Pick; - secretV2BridgeDAL: Pick; + secretV2BridgeDAL: Pick< + TSecretV2BridgeDALFactory, + "findByFolderIds" | "invalidateSecretCacheByProjectId" | "findOne" + >; + dynamicSecretDAL: Pick; }; export type TSecretFolderServiceFactory = ReturnType; @@ -61,7 +66,8 @@ export const secretFolderServiceFactory = ({ folderCommitService, projectDAL, secretApprovalPolicyService, - secretV2BridgeDAL + secretV2BridgeDAL, + dynamicSecretDAL }: TSecretFolderServiceFactoryDep) => { const createFolder = async ({ projectId, @@ -638,7 +644,8 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, environment, path: secretPath, - idOrName + idOrName, + forceDelete = false }: TDeleteFolderDTO) => { const { permission } = await permissionService.getProjectPermission({ actor, @@ -690,6 +697,22 @@ export const secretFolderServiceFactory = ({ throw new NotFoundError({ message: `Folder with ID '${idOrName}' not found` }); } + // Check if folder contains resources (secrets, dynamic secrets, subfolders) + if (!forceDelete) { + const error = new BadRequestError({ + message: `Cannot delete folder "${folderToDelete.name}" because it contains resources. Use forceDelete=true to delete it forcefully.`, + name: "deleteFolder" + }); + const secretV2 = await secretV2BridgeDAL.findOne({ folderId: folderToDelete.id }); + if (secretV2) throw error; + + const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }); + if (dynamicSecret) throw error; + + const subfolder = await folderDAL.findByParentId(folderToDelete.id); + if (subfolder) throw error; + } + const [doc] = await folderDAL.delete( { envId: env.id, diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index eed815da5..da8be52a0 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -37,6 +37,7 @@ export type TDeleteFolderDTO = { environment: string; path: string; idOrName: string; + forceDelete?: boolean; } & TProjectPermission; export type TGetFolderDTO = { diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index 3e752eed3..694a33d13 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -218,12 +218,13 @@ export const useDeleteFolder = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ path = "/", folderId, environment, projectId }) => { + mutationFn: async ({ path = "/", folderId, environment, projectId, forceDelete = true }) => { const { data } = await apiRequest.delete(`/api/v2/folders/${folderId}`, { data: { environment, projectId, - path + path, + forceDelete } }); return data; diff --git a/frontend/src/hooks/api/secretFolders/types.ts b/frontend/src/hooks/api/secretFolders/types.ts index e9ee2ae5d..f3e3cecaf 100644 --- a/frontend/src/hooks/api/secretFolders/types.ts +++ b/frontend/src/hooks/api/secretFolders/types.ts @@ -59,6 +59,7 @@ export type TDeleteFolderDTO = { environment: string; folderId: string; path?: string; + forceDelete?: boolean; }; export type TUpdateFolderBatchDTO = { From 607c085bd43b1d93171cc7714a6e03543983d48d Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Fri, 17 Oct 2025 00:32:24 +0530 Subject: [PATCH 2/5] fix: handle errors when checking for existing resources in folder deletion --- backend/src/services/secret-folder/secret-folder-service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index ba60b6d0d..f493c2565 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -703,13 +703,13 @@ export const secretFolderServiceFactory = ({ message: `Cannot delete folder "${folderToDelete.name}" because it contains resources. Use forceDelete=true to delete it forcefully.`, name: "deleteFolder" }); - const secretV2 = await secretV2BridgeDAL.findOne({ folderId: folderToDelete.id }); + const secretV2 = await secretV2BridgeDAL.findOne({ folderId: folderToDelete.id }).catch(() => null); if (secretV2) throw error; - const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }); + const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }).catch(() => null); if (dynamicSecret) throw error; - const subfolder = await folderDAL.findByParentId(folderToDelete.id); + const subfolder = await folderDAL.findByParentId(folderToDelete.id).catch(() => null); if (subfolder) throw error; } From a14c8d5b37d2d8b6d6a67cceb70c8623fc6b0cf6 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Fri, 17 Oct 2025 00:58:39 +0530 Subject: [PATCH 3/5] fix: backwards compatibility --- .../src/server/routes/v1/deprecated-secret-folder-router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/deprecated-secret-folder-router.ts b/backend/src/server/routes/v1/deprecated-secret-folder-router.ts index ecb955025..955ec06ca 100644 --- a/backend/src/server/routes/v1/deprecated-secret-folder-router.ts +++ b/backend/src/server/routes/v1/deprecated-secret-folder-router.ts @@ -318,7 +318,8 @@ export const registerDeprecatedSecretFolderRouter = async (server: FastifyZodPro ...req.body, projectId: req.body.workspaceId, idOrName: req.params.folderIdOrName, - path + path, + forceDelete: true }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, From fcd42a932257e677e97bf3571f43e9dad509a005 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Fri, 17 Oct 2025 01:32:40 +0530 Subject: [PATCH 4/5] fix: update folder deletion logic to use findOne for subfolder lookup --- backend/src/services/secret-folder/secret-folder-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index f493c2565..761c58263 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -709,7 +709,7 @@ export const secretFolderServiceFactory = ({ const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }).catch(() => null); if (dynamicSecret) throw error; - const subfolder = await folderDAL.findByParentId(folderToDelete.id).catch(() => null); + const subfolder = await folderDAL.findOne({ parentId: folderToDelete.id }).catch(() => null); if (subfolder) throw error; } From 3b07aa33b069dae749d138704790a26cc1f66a25 Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Fri, 17 Oct 2025 02:25:17 +0530 Subject: [PATCH 5/5] fix: e2e tests --- backend/e2e-test/routes/v2/secret-folder.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/e2e-test/routes/v2/secret-folder.spec.ts b/backend/e2e-test/routes/v2/secret-folder.spec.ts index a2bed759a..9cdad32fe 100644 --- a/backend/e2e-test/routes/v2/secret-folder.spec.ts +++ b/backend/e2e-test/routes/v2/secret-folder.spec.ts @@ -18,7 +18,7 @@ const createFolder = async (dto: { path: string; name: string }) => { return res.json().folder; }; -const deleteFolder = async (dto: { path: string; id: string }) => { +const deleteFolder = async (dto: { path: string; id: string; forceDelete?: boolean }) => { const res = await testServer.inject({ method: "DELETE", url: `/api/v2/folders/${dto.id}`, @@ -28,7 +28,8 @@ const deleteFolder = async (dto: { path: string; id: string }) => { body: { projectId: seedData1.project.id, environment: seedData1.environment.slug, - path: dto.path + path: dto.path, + forceDelete: dto.forceDelete ?? false } }); expect(res.statusCode).toBe(200); @@ -86,7 +87,7 @@ describe("Secret Folder Router", async () => { folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el))) }); - await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id }))); + await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id, forceDelete: true }))); }); test("Update a deep folder", async () => {