@@ -11,7 +11,7 @@ import {
1111} from '@sim/db/schema'
1212import { toError } from '@sim/utils/errors'
1313import { 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'
1515import type { ConnectorDocumentFilter } from '@/lib/api/contracts/knowledge/connectors'
1616import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution'
1717import { requireCurrentHumanRole } from '@/lib/core/application'
@@ -29,6 +29,7 @@ import {
2929import { redactKnownSensitiveValues } from '@/lib/core/security/redaction'
3030import { generateRequestId } from '@/lib/core/utils/request'
3131import { resolveCredentialTokenIdentity } from '@/lib/credentials/access'
32+ import type { DbTransaction } from '@/lib/db/types'
3233import { parseExactEnvironmentReference } from '@/lib/environment/reference'
3334import { resolveEffectiveEnvironmentVariables } from '@/lib/environment/utils'
3435import { 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 */
12821285async 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