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
18 changes: 10 additions & 8 deletions apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,23 +328,25 @@ export const DELETE = withRouteHandler(async (request: NextRequest, { params }:
const { deletedDocs, docCount } = await db.transaction(async (tx) => {
await tx.execute(sql`SELECT 1 FROM knowledge_connector WHERE id = ${connectorId} FOR UPDATE`)

// Includes pending-removal (tombstoned) docs — the connector is being
// deleted, so there's no future sync left to confirm or resurrect them.
const docs = await tx
.select({ id: document.id, fileUrl: document.fileUrl })
.from(document)
.where(
and(
eq(document.connectorId, connectorId),
isNull(document.archivedAt),
isNull(document.deletedAt)
)
)
Comment thread
waleedlatif1 marked this conversation as resolved.
.where(and(eq(document.connectorId, connectorId), isNull(document.archivedAt)))

const documentIds = docs.map((doc) => doc.id)
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 (documentIds.length > 0) {
// Kept documents become normal standalone KB entries once their connector
// is gone — resurrect any pending-removal ones rather than leaving them
// invisible tombstones with no future sync left to ever confirm or
// resurrect them.
await tx.update(document).set({ deletedAt: null }).where(inArray(document.id, documentIds))
}

const deletedConnectors = await tx
Expand Down
37 changes: 4 additions & 33 deletions apps/sim/connectors/google-sheets/google-sheets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,40 +116,26 @@ describe('googleSheetsConnector trashed handling', () => {
})

describe('listDocuments', () => {
it('returns an empty listing and confirms the empty result when the spreadsheet is trashed', async () => {
it('returns an empty listing when the spreadsheet is trashed', async () => {
stubFetch({
drive: { status: 200, body: { trashed: true, modifiedTime: '2026-07-01T00:00:00.000Z' } },
})
const syncContext: Record<string, unknown> = {}

const result = await googleSheetsConnector.listDocuments(
ACCESS_TOKEN,
SOURCE_CONFIG,
undefined,
syncContext
)
const result = await googleSheetsConnector.listDocuments(ACCESS_TOKEN, SOURCE_CONFIG)

expect(result).toEqual({ documents: [], hasMore: false })
expect(syncContext.sourceConfirmedEmpty).toBe(true)
})

it('lists every tab when the trashed field is absent', async () => {
stubFetch({ drive: { status: 200, body: { modifiedTime: '2026-07-01T00:00:00.000Z' } } })
const syncContext: Record<string, unknown> = {}

const result = await googleSheetsConnector.listDocuments(
ACCESS_TOKEN,
SOURCE_CONFIG,
undefined,
syncContext
)
const result = await googleSheetsConnector.listDocuments(ACCESS_TOKEN, SOURCE_CONFIG)

expect(result.documents.map((d) => d.externalId)).toEqual([
`${SPREADSHEET_ID}__sheet__0`,
`${SPREADSHEET_ID}__sheet__7`,
])
expect(result.hasMore).toBe(false)
expect(syncContext.sourceConfirmedEmpty).toBeUndefined()
})

it('lists every tab when trashed is explicitly false', async () => {
Expand All @@ -162,20 +148,13 @@ describe('googleSheetsConnector trashed handling', () => {

it('fails open and lists every tab when the Drive read fails', async () => {
stubFetch({ drive: { status: 500, body: { error: 'backend error' } } })
const syncContext: Record<string, unknown> = {}

const result = await googleSheetsConnector.listDocuments(
ACCESS_TOKEN,
SOURCE_CONFIG,
undefined,
syncContext
)
const result = await googleSheetsConnector.listDocuments(ACCESS_TOKEN, SOURCE_CONFIG)

expect(result.documents.map((d) => d.externalId)).toEqual([
`${SPREADSHEET_ID}__sheet__0`,
`${SPREADSHEET_ID}__sheet__7`,
])
expect(syncContext.sourceConfirmedEmpty).toBeUndefined()
})

it('fails open when the Drive body is not an object', async () => {
Expand All @@ -185,14 +164,6 @@ describe('googleSheetsConnector trashed handling', () => {

expect(result.documents).toHaveLength(2)
})

it('does not throw when trashed and no syncContext is passed', async () => {
stubFetch({ drive: { status: 200, body: { trashed: true } } })

const result = await googleSheetsConnector.listDocuments(ACCESS_TOKEN, SOURCE_CONFIG)

expect(result).toEqual({ documents: [], hasMore: false })
})
})

describe('getDocument', () => {
Expand Down
16 changes: 6 additions & 10 deletions apps/sim/connectors/google-sheets/google-sheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export const googleSheetsConnector: ConnectorConfig = {
accessToken: string,
sourceConfig: Record<string, unknown>,
_cursor?: string,
syncContext?: Record<string, unknown>
_syncContext?: Record<string, unknown>
): Promise<ExternalDocumentList> => {
const spreadsheetId = (sourceConfig.spreadsheetId as string)?.trim()
if (!spreadsheetId) {
Expand All @@ -269,18 +269,14 @@ export const googleSheetsConnector: ConnectorConfig = {

/**
* A trashed spreadsheet is no longer current content, so it drops out of the
* listing and stops being re-indexed. Unlike an ordinary empty listing page
* (which could equally mean the source is unreachable), this is a direct,
* single-resource confirmation from the Drive API that the spreadsheet itself
* is gone — so `sourceConfirmedEmpty` tells the sync engine's zero-document
* guard it's safe to reconcile (purge the stored tabs) on this sync, rather
* than requiring a forced full resync. `validateConfig` reports the trashed
* state so the connector does not look healthy while serving tabs from a file
* its owner has thrown away.
* listing and stops being re-indexed. The sync engine reconciles its absence
* the same way it does for every connector: pending-removal on the first
* sync that doesn't see it, purged once a later sync confirms it's still
* gone. `validateConfig` reports the trashed state so the connector does not
* look healthy while serving tabs from a file its owner has thrown away.
*/
if (isTrashedDriveFile(driveMetadata)) {
logger.info('Spreadsheet is in the Drive trash; listing no documents', { spreadsheetId })
if (syncContext) syncContext.sourceConfirmedEmpty = true
return { documents: [], hasMore: false }
}

Expand Down
Loading
Loading