Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ describe('Knowledge Connector By ID API Route', () => {
.mockReturnValueOnce(mockDbChain)
.mockResolvedValueOnce([{ id: 'doc-1', fileUrl: '/api/uploads/test.txt' }])
.mockReturnValueOnce(mockDbChain)
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456', connectorType: 'jira' }])
mockDbChain.returning.mockResolvedValueOnce([{ id: 'conn-456' }])

const req = createMockRequest('DELETE')
Expand Down
42 changes: 29 additions & 13 deletions apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,10 @@ 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 { searchParams } = new URL(request.url)
const deleteDocuments = 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
Expand All @@ -306,10 +309,12 @@ 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))
}
}

const deletedConnectors = await tx
Expand All @@ -328,16 +333,23 @@ 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)

await cleanupUnusedTagDefinitions(knowledgeBaseId, requestId).catch((error) => {
logger.warn(`[${requestId}] Failed to cleanup tag definitions`, error)
})
if (deleteDocuments) {
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(`[${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,
Expand All @@ -349,7 +361,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,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
<div className='flex flex-col gap-3'>
{/* Auth: API key input or OAuth credential selection */}
{isApiKeyMode ? (
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<Label>
{connectorConfig.auth.mode === 'apiKey' && connectorConfig.auth.label
? connectorConfig.auth.label
Expand All @@ -394,7 +394,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
/>
</div>
) : (
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<Label>Account</Label>
{credentialsLoading ? (
<div className='flex items-center gap-2 text-[var(--text-muted)] text-small'>
Expand Down Expand Up @@ -442,7 +442,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
canonicalId && (canonicalGroups.get(canonicalId)?.length ?? 0) === 2

return (
<div key={field.id} className='flex flex-col gap-1'>
<div key={field.id} className='flex flex-col gap-2'>
<div className='flex items-center justify-between'>
<Label>
{field.title}
Expand Down Expand Up @@ -507,7 +507,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo

{/* Tag definitions (opt-out) */}
{connectorConfig.tagDefinitions && connectorConfig.tagDefinitions.length > 0 && (
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<Label>Metadata Tags</Label>
{connectorConfig.tagDefinitions.map((tagDef) => (
<div
Expand Down Expand Up @@ -550,7 +550,7 @@ export function AddConnectorModal({ open, onOpenChange, knowledgeBaseId }: AddCo
)}

{/* Sync interval */}
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<Label>Sync Frequency</Label>
<ButtonGroup
value={String(syncInterval)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import {
Badge,
Button,
Checkbox,
Modal,
ModalBody,
ModalContent,
Expand Down Expand Up @@ -77,6 +78,12 @@ export function ConnectorsSection({
const { mutate: updateConnector } = useUpdateConnector()
const { mutate: deleteConnector, isPending: isDeleting } = useDeleteConnector()
const [deleteTarget, setDeleteTarget] = useState<string | null>(null)
const [deleteDocuments, setDeleteDocuments] = useState(false)

const closeDeleteModal = useCallback(() => {
setDeleteTarget(null)
setDeleteDocuments(false)
}, [])
const [editingConnector, setEditingConnector] = useState<ConnectorData | null>(null)
const [error, setError] = useState<string | null>(null)
const [syncingIds, setSyncingIds] = useState<Set<string>>(() => new Set())
Expand Down Expand Up @@ -224,22 +231,30 @@ export function ConnectorsSection({
/>
)}

<Modal open={deleteTarget !== null} onOpenChange={() => setDeleteTarget(null)}>
<Modal open={deleteTarget !== null} onOpenChange={closeDeleteModal}>
<ModalContent size='sm'>
<ModalHeader>Delete Connector</ModalHeader>
<ModalHeader>Remove Connector</ModalHeader>
<ModalBody>
<p className='text-[var(--text-secondary)] text-sm'>
Are you sure you want to remove this connected source?{' '}
<span className='text-[var(--text-error)]'>
This will stop future syncs from this source.
</span>{' '}
<span className='text-[var(--text-tertiary)]'>
Documents already synced will remain in the knowledge base.
</span>
This will disconnect the source and stop future syncs. Documents already synced will
remain in the knowledge base unless you choose to delete them.
</p>
<div className='mt-3 flex items-center gap-2'>
<Checkbox
id='delete-docs'
checked={deleteDocuments}
onCheckedChange={(checked) => setDeleteDocuments(checked === true)}
/>
<label
htmlFor='delete-docs'
className='cursor-pointer text-[var(--text-secondary)] text-sm'
>
Also delete all synced documents
</label>
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={() => setDeleteTarget(null)} disabled={isDeleting}>
<Button variant='default' onClick={closeDeleteModal} disabled={isDeleting}>
Cancel
</Button>
<Button
Expand All @@ -248,23 +263,23 @@ export function ConnectorsSection({
onClick={() => {
if (deleteTarget) {
deleteConnector(
{ knowledgeBaseId, connectorId: deleteTarget },
{ knowledgeBaseId, connectorId: deleteTarget, deleteDocuments },
{
onSuccess: () => {
setError(null)
setDeleteTarget(null)
closeDeleteModal()
},
onError: (err) => {
logger.error('Delete connector failed', { error: err.message })
setError(err.message)
setDeleteTarget(null)
closeDeleteModal()
},
}
)
}
}}
>
{isDeleting ? 'Deleting...' : 'Delete'}
{isDeleting ? 'Removing...' : 'Remove'}
</Button>
</ModalFooter>
</ModalContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function SettingsTab({
return (
<div className='flex flex-col gap-3'>
{connectorConfig?.configFields.map((field) => (
<div key={field.id} className='flex flex-col gap-1'>
<div key={field.id} className='flex flex-col gap-2'>
<Label>
{field.title}
{field.required && <span className='ml-0.5 text-[var(--text-error)]'>*</span>}
Expand Down Expand Up @@ -227,7 +227,7 @@ function SettingsTab({
</div>
))}

<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-2'>
<Label>Sync Frequency</Label>
<ButtonGroup
value={String(syncInterval)}
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/hooks/queries/kb/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,16 @@ export function useUpdateConnector() {
export interface DeleteConnectorParams {
knowledgeBaseId: string
connectorId: string
deleteDocuments?: boolean
}

async function deleteConnector({
knowledgeBaseId,
connectorId,
deleteDocuments,
}: DeleteConnectorParams): Promise<void> {
const response = await fetch(`/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`, {
const base = `/api/knowledge/${knowledgeBaseId}/connectors/${connectorId}`
const response = await fetch(deleteDocuments ? `${base}?deleteDocuments=true` : base, {
method: 'DELETE',
})

Expand Down
37 changes: 10 additions & 27 deletions apps/sim/lib/knowledge/documents/parser-extension.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,26 @@
import { getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils'
import {
isAlphanumericExtension,
isSupportedExtension,
SUPPORTED_DOCUMENT_EXTENSIONS,
} from '@/lib/uploads/utils/validation'

const SUPPORTED_FILE_TYPES = [
'pdf',
'csv',
'docx',
'doc',
'txt',
'md',
'xlsx',
'xls',
'pptx',
'ppt',
'html',
'htm',
'json',
'yaml',
'yml',
] as const

const SUPPORTED_FILE_TYPES_TEXT = SUPPORTED_FILE_TYPES.join(', ')

function isSupportedParserExtension(extension: string): boolean {
return SUPPORTED_FILE_TYPES.includes(extension as (typeof SUPPORTED_FILE_TYPES)[number])
}
const SUPPORTED_EXTENSIONS_TEXT = SUPPORTED_DOCUMENT_EXTENSIONS.join(', ')

export function resolveParserExtension(
filename: string,
mimeType?: string,
fallback?: string
): string {
const raw = filename.includes('.') ? filename.split('.').pop()?.toLowerCase() : undefined
const filenameExtension = raw && /^[a-z0-9]+$/.test(raw) ? raw : undefined
const filenameExtension = raw && isAlphanumericExtension(raw) ? raw : undefined

if (filenameExtension && isSupportedParserExtension(filenameExtension)) {
if (filenameExtension && isSupportedExtension(filenameExtension)) {
return filenameExtension
}

const mimeExtension = mimeType ? getExtensionFromMimeType(mimeType) : undefined
if (mimeExtension && isSupportedParserExtension(mimeExtension)) {
if (mimeExtension && isSupportedExtension(mimeExtension)) {
return mimeExtension
}

Expand All @@ -47,7 +30,7 @@ export function resolveParserExtension(

if (filenameExtension) {
throw new Error(
`Unsupported file type: ${filenameExtension}. Supported types are: ${SUPPORTED_FILE_TYPES_TEXT}`
`Unsupported file type: ${filenameExtension}. Supported types are: ${SUPPORTED_EXTENSIONS_TEXT}`
)
}

Expand Down
13 changes: 11 additions & 2 deletions apps/sim/lib/uploads/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import path from 'path'

/**
* Checks whether a string is a valid file extension (lowercase alphanumeric only).
* Rejects extensions containing spaces, punctuation, or other non-alphanumeric characters
* that arise from non-filename document names (e.g. "Sim.ai <> RVTech").
*/
export function isAlphanumericExtension(ext: string): boolean {
return /^[a-z0-9]+$/.test(ext)
}

export const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB

export const SUPPORTED_DOCUMENT_EXTENSIONS = [
Expand Down Expand Up @@ -138,7 +147,7 @@ export interface FileValidationError {
*/
export function validateFileType(fileName: string, mimeType: string): FileValidationError | null {
const raw = path.extname(fileName).toLowerCase().substring(1)
const extension = (/^[a-z0-9]+$/.test(raw) ? raw : '') as SupportedDocumentExtension
const extension = (isAlphanumericExtension(raw) ? raw : '') as SupportedDocumentExtension

if (!SUPPORTED_DOCUMENT_EXTENSIONS.includes(extension)) {
return {
Expand Down Expand Up @@ -223,7 +232,7 @@ export function validateMediaFileType(
mimeType: string
): FileValidationError | null {
const raw = path.extname(fileName).toLowerCase().substring(1)
const extension = /^[a-z0-9]+$/.test(raw) ? raw : ''
const extension = isAlphanumericExtension(raw) ? raw : ''

const isAudio = SUPPORTED_AUDIO_EXTENSIONS.includes(extension as SupportedAudioExtension)
const isVideo = SUPPORTED_VIDEO_EXTENSIONS.includes(extension as SupportedVideoExtension)
Expand Down
3 changes: 3 additions & 0 deletions packages/db/migrations/0182_luxuriant_quasar.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE "document" DROP CONSTRAINT "document_connector_id_knowledge_connector_id_fk";
--> statement-breakpoint
ALTER TABLE "document" ADD CONSTRAINT "document_connector_id_knowledge_connector_id_fk" FOREIGN KEY ("connector_id") REFERENCES "public"."knowledge_connector"("id") ON DELETE set null ON UPDATE no action;
Loading
Loading