Skip to content

Commit 55d2840

Browse files
committed
fix(knowledge): check the image setting and restore in one locked transaction
1 parent f273106 commit 55d2840

2 files changed

Lines changed: 33 additions & 29 deletions

File tree

‎apps/sim/lib/knowledge/__integration__/confluence-images.integration.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ describe('Confluence image attachments through sync, indexing and reconciliation
183183
function setExcluded(documentId: string, operation: 'exclude' | 'restore') {
184184
return updateKnowledgeConnectorDocuments.execute({
185185
principal: { kind: 'session', userId: ids.aliceId, sessionId: 'confluence-images' },
186-
input: { connectorId: ids.connectorId, operation, documentIds: [documentId] },
186+
input: {
187+
knowledgeBaseId: ids.knowledgeBaseId,
188+
connectorId: ids.connectorId,
189+
operation,
190+
documentIds: [documentId],
191+
},
187192
})
188193
}
189194
async function imageRemovalPending() {

‎apps/sim/lib/knowledge/application/connectors.ts‎

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@sim/db/schema'
1212
import { toError } from '@sim/utils/errors'
1313
import { escapeLikePattern } from '@sim/utils/string'
14-
import { and, asc, desc, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm'
14+
import { and, asc, desc, eq, inArray, isNull, lt, or, type SQL, sql } from 'drizzle-orm'
1515
import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors'
1616
import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
1717
import { requireCurrentHumanRole } from '@/lib/core/application'
@@ -29,6 +29,7 @@ import {
2929
import { redactKnownSensitiveValues } from '@/lib/core/security/redaction'
3030
import { generateRequestId } from '@/lib/core/utils/request'
3131
import { resolveCredentialTokenIdentity } from '@/lib/credentials/access'
32+
import type { DbTransaction } from '@/lib/db/types'
3233
import { parseExactEnvironmentReference } from '@/lib/environment/reference'
3334
import { resolveEffectiveEnvironmentVariables } from '@/lib/environment/utils'
3435
import { requireKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
@@ -1277,35 +1278,32 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase(
12771278
/**
12781279
* An excluded image keeps its indexed text, and turning image attachments off
12791280
* skips excluded rows so the exclusion survives turning them back on. Restoring
1280-
* one while images are off would re-enable text the source no longer lists.
1281+
* one while images are off would re-enable text the source no longer lists. The
1282+
* connector row stays locked until the restore commits, so a concurrent change of
1283+
* the setting either sees the restored image or is seen by this check.
12811284
*/
12821285
async function assertImageRestoreAllowed(
1286+
tx: DbTransaction,
12831287
connector: { id: string; connectorType: string },
1284-
documentIds: string[]
1288+
restorable: SQL | undefined
12851289
) {
12861290
const imageExtraction = getConnectorMeta(connector.connectorType)?.imageExtraction
12871291
if (!imageExtraction) return
1288-
const [stored] = await db
1292+
const [stored] = await tx
12891293
.select({ sourceConfig: knowledgeConnector.sourceConfig })
12901294
.from(knowledgeConnector)
12911295
.where(eq(knowledgeConnector.id, connector.id))
1292-
.limit(1)
1296+
.for('update')
12931297
if (
12941298
!stored ||
12951299
isImageExtractionEnabled(imageExtraction, stored.sourceConfig as Record<string, unknown>)
12961300
) {
12971301
return
12981302
}
1299-
const [image] = await db
1303+
const [image] = await tx
13001304
.select({ id: document.id })
13011305
.from(document)
1302-
.where(
1303-
and(
1304-
eq(document.connectorId, connector.id),
1305-
inArray(document.id, documentIds),
1306-
inArray(document.mimeType, [...SEMANTIC_IMAGE_MIME_TYPES])
1307-
)
1308-
)
1306+
.where(and(restorable, inArray(document.mimeType, [...SEMANTIC_IMAGE_MIME_TYPES])))
13091307
.limit(1)
13101308
if (image) {
13111309
throw new OrchestrationError(
@@ -1339,21 +1337,22 @@ export const updateKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCas
13391337
if (restoring && !requiresConnectorIndexing(context.knowledgeBase.isSearchIndex)) {
13401338
throw new OrchestrationError('validation', 'This search index is inactive; use Sim Search.')
13411339
}
1342-
if (restoring) await assertImageRestoreAllowed(context.connector, documentIds)
1343-
const updated = await db
1344-
.update(document)
1345-
.set({ userExcluded: !restoring, enabled: restoring })
1346-
.where(
1347-
and(
1348-
eq(document.connectorId, context.connectorId),
1349-
inArray(document.id, documentIds),
1350-
eq(document.userExcluded, restoring),
1351-
isNull(document.archivedAt),
1352-
isNull(document.deletedAt),
1353-
knowledgeAccessCondition(await context.access.get())
1354-
)
1355-
)
1356-
.returning({ id: document.id })
1340+
const target = and(
1341+
eq(document.connectorId, context.connectorId),
1342+
inArray(document.id, documentIds),
1343+
eq(document.userExcluded, restoring),
1344+
isNull(document.archivedAt),
1345+
isNull(document.deletedAt),
1346+
knowledgeAccessCondition(await context.access.get())
1347+
)
1348+
const updated = await db.transaction(async (tx) => {
1349+
if (restoring) await assertImageRestoreAllowed(tx, context.connector, target)
1350+
return tx
1351+
.update(document)
1352+
.set({ userExcluded: !restoring, enabled: restoring })
1353+
.where(target)
1354+
.returning({ id: document.id })
1355+
})
13571356
return {
13581357
operation: input.operation,
13591358
count: updated.length,

0 commit comments

Comments
 (0)