Skip to content

Commit 851d265

Browse files
committed
fix(knowledge): take the image restore locks in sync's order
1 parent 55d2840 commit 851d265

1 file changed

Lines changed: 29 additions & 29 deletions

File tree

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

Lines changed: 29 additions & 29 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, type SQL, sql } from 'drizzle-orm'
14+
import { and, asc, desc, eq, inArray, isNull, lt, or, 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'
@@ -1278,34 +1278,31 @@ export const listKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCase(
12781278
/**
12791279
* An excluded image keeps its indexed text, and turning image attachments off
12801280
* skips excluded rows so the exclusion survives turning them back on. Restoring
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.
1281+
* one while images are off would re-enable text the source no longer lists.
1282+
*
1283+
* Runs after the restore's document update in the same transaction, matching a
1284+
* sync's lock order (documents, then a share lock on the connector). The share
1285+
* lock holds off a concurrent change of the setting until the restore commits,
1286+
* so that change queues removal of the restored image or this check sees it.
12841287
*/
12851288
async function assertImageRestoreAllowed(
12861289
tx: DbTransaction,
12871290
connector: { id: string; connectorType: string },
1288-
restorable: SQL | undefined
1291+
restored: { mimeType: string }[]
12891292
) {
12901293
const imageExtraction = getConnectorMeta(connector.connectorType)?.imageExtraction
1291-
if (!imageExtraction) return
1294+
if (!imageExtraction || !restored.some((row) => SEMANTIC_IMAGE_MIME_TYPES.has(row.mimeType))) {
1295+
return
1296+
}
12921297
const [stored] = await tx
12931298
.select({ sourceConfig: knowledgeConnector.sourceConfig })
12941299
.from(knowledgeConnector)
12951300
.where(eq(knowledgeConnector.id, connector.id))
1296-
.for('update')
1301+
.for('share')
12971302
if (
1298-
!stored ||
1299-
isImageExtractionEnabled(imageExtraction, stored.sourceConfig as Record<string, unknown>)
1303+
stored &&
1304+
!isImageExtractionEnabled(imageExtraction, stored.sourceConfig as Record<string, unknown>)
13001305
) {
1301-
return
1302-
}
1303-
const [image] = await tx
1304-
.select({ id: document.id })
1305-
.from(document)
1306-
.where(and(restorable, inArray(document.mimeType, [...SEMANTIC_IMAGE_MIME_TYPES])))
1307-
.limit(1)
1308-
if (image) {
13091306
throw new OrchestrationError(
13101307
'validation',
13111308
'Turn on image attachments for this connector before restoring an image.'
@@ -1337,21 +1334,24 @@ export const updateKnowledgeConnectorDocuments = defineAuthorizedKnowledgeUseCas
13371334
if (restoring && !requiresConnectorIndexing(context.knowledgeBase.isSearchIndex)) {
13381335
throw new OrchestrationError('validation', 'This search index is inactive; use Sim Search.')
13391336
}
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-
)
1337+
const access = await context.access.get()
13481338
const updated = await db.transaction(async (tx) => {
1349-
if (restoring) await assertImageRestoreAllowed(tx, context.connector, target)
1350-
return tx
1339+
const rows = await tx
13511340
.update(document)
13521341
.set({ userExcluded: !restoring, enabled: restoring })
1353-
.where(target)
1354-
.returning({ id: document.id })
1342+
.where(
1343+
and(
1344+
eq(document.connectorId, context.connectorId),
1345+
inArray(document.id, documentIds),
1346+
eq(document.userExcluded, restoring),
1347+
isNull(document.archivedAt),
1348+
isNull(document.deletedAt),
1349+
knowledgeAccessCondition(access)
1350+
)
1351+
)
1352+
.returning({ id: document.id, mimeType: document.mimeType })
1353+
if (restoring) await assertImageRestoreAllowed(tx, context.connector, rows)
1354+
return rows
13551355
})
13561356
return {
13571357
operation: input.operation,

0 commit comments

Comments
 (0)