@@ -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 , type SQL , sql } from 'drizzle-orm'
14+ import { and , asc , desc , eq , inArray , isNull , lt , or , 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'
@@ -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 */
12851288async 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