From 2e48eaa8b9b2511754f435b0f9d3f34e5c2933c1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 11:18:42 -0700 Subject: [PATCH 1/7] fix(knowledge): give users choice to keep or delete documents when removing connector --- .../[id]/connectors/[connectorId]/route.ts | 43 +++++++++++----- .../connectors-section/connectors-section.tsx | 49 ++++++++++++++----- apps/sim/hooks/queries/kb/connectors.ts | 7 ++- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 0d8212c98fd..3db978b9b79 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -292,7 +292,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'Connector not found' }, { status: 404 }) } - const connectorDocuments = await db.transaction(async (tx) => { + const deleteDocuments = request.nextUrl.searchParams.get('deleteDocuments') === 'true' + + const { deletedDocs, docCount } = await db.transaction(async (tx) => { await tx.execute(sql`SELECT 1 FROM knowledge_connector WHERE id = ${connectorId} FOR UPDATE`) const docs = await tx @@ -306,10 +308,17 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { ) ) - const documentIds = docs.map((doc) => doc.id) - if (documentIds.length > 0) { - await tx.delete(embedding).where(inArray(embedding.documentId, documentIds)) - await tx.delete(document).where(inArray(document.id, documentIds)) + if (deleteDocuments) { + const documentIds = docs.map((doc) => doc.id) + if (documentIds.length > 0) { + await tx.delete(embedding).where(inArray(embedding.documentId, documentIds)) + await tx.delete(document).where(inArray(document.id, documentIds)) + } + } else if (docs.length > 0) { + await tx + .update(document) + .set({ connectorId: null }) + .where(eq(document.connectorId, connectorId)) } const deletedConnectors = await tx @@ -328,16 +337,22 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { throw new Error('Connector not found') } - return docs + return { deletedDocs: deleteDocuments ? docs : [], docCount: docs.length } }) - await deleteDocumentStorageFiles(connectorDocuments, requestId) + if (deletedDocs.length > 0) { + await deleteDocumentStorageFiles(deletedDocs, requestId) + } - await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId).catch((error) => { - logger.warn(`[${requestId}] Failed to cleanup tag definitions`, error) - }) + if (deleteDocuments) { + await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId).catch((error) => { + logger.warn(`[${requestId}] Failed to cleanup tag definitions`, error) + }) + } - logger.info(`[${requestId}] Hard-deleted connector ${connectorId} and its documents`) + logger.info( + `[${requestId}] Deleted connector ${connectorId}${deleteDocuments ? ` and ${docCount} documents` : `, kept ${docCount} documents`}` + ) recordAudit({ workspaceId: writeCheck.knowledgeBase.workspaceId, @@ -349,7 +364,11 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { resourceId: connectorId, resourceName: existingConnector[0].connectorType, description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`, - metadata: { knowledgeBaseId, documentsDeleted: connectorDocuments.length }, + metadata: { + knowledgeBaseId, + documentsDeleted: deleteDocuments ? docCount : 0, + documentsKept: deleteDocuments ? 0 : docCount, + }, request, }) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index b8db5cffcc3..579b0b8da67 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -18,6 +18,7 @@ import { import { Badge, Button, + Checkbox, Modal, ModalBody, ModalContent, @@ -77,6 +78,7 @@ export function ConnectorsSection({ const { mutate: updateConnector } = useUpdateConnector() const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector() const [deleteTarget, setDeleteTarget] = useState(null) + const [deleteDocuments, setDeleteDocuments] = useState(false) const [editingConnector, setEditingConnector] = useState(null) const [error, setError] = useState(null) const [syncingIds, setSyncingIds] = useState>(() => new Set()) @@ -224,22 +226,43 @@ export function ConnectorsSection({ /> )} - setDeleteTarget(null)}> + { + setDeleteTarget(null) + setDeleteDocuments(false) + }} + > - Delete Connector + Remove Connector

- Are you sure you want to remove this connected source?{' '} - - This will stop future syncs from this source. - {' '} - - Documents already synced will remain in the knowledge base. - + This will disconnect the source and stop future syncs. Documents already synced will + remain in the knowledge base unless you choose to delete them.

+
+ setDeleteDocuments(checked === true)} + /> + +
-
diff --git a/apps/sim/hooks/queries/kb/connectors.ts b/apps/sim/hooks/queries/kb/connectors.ts index 1b737d8f2a5..4df64b2ea62 100644 --- a/apps/sim/hooks/queries/kb/connectors.ts +++ b/apps/sim/hooks/queries/kb/connectors.ts @@ -224,13 +224,18 @@ export function useUpdateConnector() { export interface DeleteConnectorParams { knowledgeBaseId: string connectorId: string + deleteDocuments?: boolean } async function deleteConnector({ knowledgeBaseId, connectorId, + deleteDocuments, }: DeleteConnectorParams): Promise { - const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`, { + const url = deleteDocuments + ? `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}?deleteDocuments=true` + : `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}` + const response = await fetch(url, { method: 'DELETE', }) From 05d8c1872bb8f3d7306205a1737011b17a978166 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 28 Mar 2026 11:26:53 -0700 Subject: [PATCH 2/7] refactor(knowledge): clean up connector delete and extract shared extension validator - Extract `isAlphanumericExtension` helper to deduplicate regex across parser-extension.ts and validation.ts - Extract `closeDeleteModal` callback to eliminate 4x scattered state resets - Add archivedAt/deletedAt filters to UPDATE query in keep-docs delete path - Parallelize storage file cleanup and tag definition cleanup with Promise.all - Deduplicate URL construction in delete connector hook Co-Authored-By: Claude Opus 4.6 --- .../[id]/connectors/[connectorId]/route.ts | 23 +++++++++------ .../connectors-section/connectors-section.tsx | 28 ++++++------------- apps/sim/hooks/queries/kb/connectors.ts | 6 ++-- .../knowledge/documents/parser-extension.ts | 3 +- apps/sim/lib/uploads/utils/validation.ts | 13 +++++++-- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 3db978b9b79..d288272b2b0 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -318,7 +318,13 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { await tx .update(document) .set({ connectorId: null }) - .where(eq(document.connectorId, connectorId)) + .where( + and( + eq(document.connectorId, connectorId), + isNull(document.archivedAt), + isNull(document.deletedAt) + ) + ) } const deletedConnectors = await tx @@ -340,14 +346,15 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return { deletedDocs: deleteDocuments ? docs : [], docCount: docs.length } }) - if (deletedDocs.length > 0) { - await deleteDocumentStorageFiles(deletedDocs, requestId) - } - if (deleteDocuments) { - await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId).catch((error) => { - logger.warn(`[${requestId}] Failed to cleanup tag definitions`, error) - }) + await Promise.all([ + deletedDocs.length > 0 + ? deleteDocumentStorageFiles(deletedDocs, requestId) + : Promise.resolve(), + cleanupUnusedTagDefinitions(knowledgeBaseId, requestId).catch((error) => { + logger.warn(`[${requestId}] Failed to cleanup tag definitions`, error) + }), + ]) } logger.info( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx index 579b0b8da67..f7eeac07c8b 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx @@ -79,6 +79,11 @@ export function ConnectorsSection({ const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector() const [deleteTarget, setDeleteTarget] = useState(null) const [deleteDocuments, setDeleteDocuments] = useState(false) + + const closeDeleteModal = useCallback(() => { + setDeleteTarget(null) + setDeleteDocuments(false) + }, []) const [editingConnector, setEditingConnector] = useState(null) const [error, setError] = useState(null) const [syncingIds, setSyncingIds] = useState>(() => new Set()) @@ -226,13 +231,7 @@ export function ConnectorsSection({ /> )} - { - setDeleteTarget(null) - setDeleteDocuments(false) - }} - > + Remove Connector @@ -255,14 +254,7 @@ export function ConnectorsSection({ -