@@ -34,7 +34,7 @@ import { generateRestoreName } from '@/lib/core/utils/restore-name'
3434import { findActiveFolder , resolveRestoredFolderId } from '@/lib/folders/queries'
3535import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
3636import { knowledgeAccessCondition } from '@/lib/knowledge/access/predicate'
37- import { MAX_KNOWLEDGE_ACCESS_CANDIDATES } from '@/lib/knowledge/access/types'
37+ import type { KnowledgeAccessProvider } from '@/lib/knowledge/access/types'
3838import { mirrorsSourceAcls } from '@/lib/knowledge/connectors/access-modes'
3939import { type KnowledgeReadAccess , knowledgeReadAccessBatches } from '@/lib/knowledge/read-access'
4040import type {
@@ -174,6 +174,7 @@ async function readKnowledgeBaseRows(
174174) : Promise <
175175 Array < Omit < KnowledgeBaseWithCounts , 'connectorTypes' | 'hasPermissionScopedConnector' > >
176176> {
177+ const scope = access && 'get' in access ? await access . get ( ) : access
177178 const query = db
178179 . select ( {
179180 id : knowledgeBase . id ,
@@ -201,7 +202,7 @@ async function readKnowledgeBaseRows(
201202 eq ( document . userExcluded , false ) ,
202203 isNull ( document . archivedAt ) ,
203204 isNull ( document . deletedAt ) ,
204- access ? ( 'get' in access ? sql `false` : knowledgeAccessCondition ( access ) ) : undefined
205+ scope ? knowledgeAccessCondition ( scope ) : undefined
205206 )
206207 )
207208 . where ( where )
@@ -210,59 +211,85 @@ async function readKnowledgeBaseRows(
210211
211212 const rows = limit === undefined ? await query : await query . limit ( limit )
212213
213- const counts =
214- access && 'get' in access
215- ? await readKnowledgeBaseDocumentCounts (
216- rows . map ( ( kb ) => kb . id ) ,
214+ /**
215+ * The join above already counted everything the reader's stored ACL admits. Only a
216+ * provider can add documents a live source (GitHub, Confluence) authorizes beyond that,
217+ * and that supplement is resolved once for the whole list: an unpaged list is bounded by
218+ * its own filter, a page by its row IDs, so a workspace with tens of thousands of bases
219+ * never turns into hundreds of per-batch round trips.
220+ */
221+ const liveCounts =
222+ access && 'get' in access && rows . length > 0
223+ ? await readLiveSourceDocumentCounts (
224+ limit === undefined && where
225+ ? where
226+ : inArray (
227+ knowledgeBase . id ,
228+ rows . map ( ( kb ) => kb . id )
229+ ) ,
217230 access
218231 )
219232 : undefined
220233 return rows . map ( ( kb ) => ( {
221234 ...kb ,
222235 chunkingConfig : kb . chunkingConfig as ChunkingConfig ,
223- docCount : counts ? ( counts . get ( kb . id ) ?. docCount ?? 0 ) : Number ( kb . docCount ) ,
224- tokenCount : counts ? ( counts . get ( kb . id ) ?. tokenCount ?? 0 ) : kb . tokenCount ,
236+ docCount : Number ( kb . docCount ) + ( liveCounts ? .get ( kb . id ) ?. docCount ?? 0 ) ,
237+ tokenCount : kb . tokenCount + ( liveCounts ? .get ( kb . id ) ?. tokenCount ?? 0 ) ,
225238 } ) )
226239}
227240
228- /** Counts only hydrated access batches, keeping candidate discovery free of document metadata. */
229- async function readKnowledgeBaseDocumentCounts (
230- knowledgeBaseIds : readonly string [ ] ,
231- access : KnowledgeReadAccess
241+ const ACTIVE_DOCUMENT_CONDITIONS = [
242+ eq ( document . userExcluded , false ) ,
243+ isNull ( document . archivedAt ) ,
244+ isNull ( document . deletedAt ) ,
245+ ] as const
246+
247+ /**
248+ * Document totals per knowledge base for one access predicate, restricted to the bases
249+ * `subject` selects. `subject` may reference `knowledge_base` columns.
250+ */
251+ async function countDocumentsByKnowledgeBase (
252+ subject : SQL ,
253+ accessCondition : SQL
254+ ) : Promise < Array < { knowledgeBaseId : string ; docCount : number ; tokenCount : number } > > {
255+ return db
256+ . select ( {
257+ knowledgeBaseId : document . knowledgeBaseId ,
258+ docCount : count ( ) ,
259+ tokenCount : sql < number > `COALESCE(SUM(${ document . tokenCount } ), 0)` . mapWith ( Number ) ,
260+ } )
261+ . from ( document )
262+ . innerJoin ( knowledgeBase , eq ( document . knowledgeBaseId , knowledgeBase . id ) )
263+ . where ( and ( subject , ...ACTIVE_DOCUMENT_CONDITIONS , accessCondition ) )
264+ . groupBy ( document . knowledgeBaseId )
265+ }
266+
267+ /**
268+ * Totals for documents only a live source authorizes, on top of the reader's stored ACL.
269+ * The ordinary predicate is skipped because every caller has already counted it; candidate
270+ * discovery stays free of document metadata and returns nothing for a reader without
271+ * live-source credentials.
272+ */
273+ async function readLiveSourceDocumentCounts (
274+ subject : SQL ,
275+ access : KnowledgeAccessProvider
232276) : Promise < Map < string , { docCount : number ; tokenCount : number } > > {
233277 const counts = new Map < string , { docCount : number ; tokenCount : number } > ( )
234- for (
235- let offset = 0 ;
236- offset < knowledgeBaseIds . length ;
237- offset += MAX_KNOWLEDGE_ACCESS_CANDIDATES
238- ) {
239- const conditions = [
240- inArray (
241- knowledgeBase . id ,
242- knowledgeBaseIds . slice ( offset , offset + MAX_KNOWLEDGE_ACCESS_CANDIDATES )
243- ) ,
244- eq ( document . userExcluded , false ) ,
245- isNull ( document . archivedAt ) ,
246- isNull ( document . deletedAt ) ,
247- ]
248- for await ( const accessCondition of knowledgeReadAccessBatches ( access , conditions ) ) {
249- const rows = await db
250- . select ( {
251- knowledgeBaseId : document . knowledgeBaseId ,
252- docCount : count ( ) ,
253- tokenCount : sql < number > `COALESCE(SUM(${ document . tokenCount } ), 0)` . mapWith ( Number ) ,
254- } )
255- . from ( document )
256- . innerJoin ( knowledgeBase , eq ( document . knowledgeBaseId , knowledgeBase . id ) )
257- . where ( and ( ...conditions , accessCondition ) )
258- . groupBy ( document . knowledgeBaseId )
259- for ( const row of rows ) {
260- const previous = counts . get ( row . knowledgeBaseId )
261- counts . set ( row . knowledgeBaseId , {
262- docCount : ( previous ?. docCount ?? 0 ) + Number ( row . docCount ) ,
263- tokenCount : ( previous ?. tokenCount ?? 0 ) + Number ( row . tokenCount ) ,
264- } )
265- }
278+ let ordinary = true
279+ for await ( const accessCondition of knowledgeReadAccessBatches ( access , [
280+ subject ,
281+ ...ACTIVE_DOCUMENT_CONDITIONS ,
282+ ] ) ) {
283+ if ( ordinary ) {
284+ ordinary = false
285+ continue
286+ }
287+ for ( const row of await countDocumentsByKnowledgeBase ( subject , accessCondition ) ) {
288+ const previous = counts . get ( row . knowledgeBaseId )
289+ counts . set ( row . knowledgeBaseId , {
290+ docCount : ( previous ?. docCount ?? 0 ) + Number ( row . docCount ) ,
291+ tokenCount : ( previous ?. tokenCount ?? 0 ) + Number ( row . tokenCount ) ,
292+ } )
266293 }
267294 }
268295 return counts
@@ -1019,11 +1046,14 @@ export async function attachKnowledgeBaseConnectors(
10191046) : Promise < KnowledgeBaseWithCounts > {
10201047 let visible = knowledgeBase
10211048 if ( access ) {
1022- const counts = await readKnowledgeBaseDocumentCounts ( [ knowledgeBase . id ] , access )
1049+ const subject = eq ( document . knowledgeBaseId , knowledgeBase . id )
1050+ const scope = 'get' in access ? await access . get ( ) : access
1051+ const [ ordinary ] = await countDocumentsByKnowledgeBase ( subject , knowledgeAccessCondition ( scope ) )
1052+ const live = 'get' in access ? await readLiveSourceDocumentCounts ( subject , access ) : undefined
10231053 visible = {
10241054 ...knowledgeBase ,
1025- docCount : counts . get ( knowledgeBase . id ) ?. docCount ?? 0 ,
1026- tokenCount : counts . get ( knowledgeBase . id ) ?. tokenCount ?? 0 ,
1055+ docCount : Number ( ordinary ?. docCount ?? 0 ) + ( live ?. get ( knowledgeBase . id ) ?. docCount ?? 0 ) ,
1056+ tokenCount : ( ordinary ?. tokenCount ?? 0 ) + ( live ?. get ( knowledgeBase . id ) ?. tokenCount ?? 0 ) ,
10271057 }
10281058 }
10291059 const [ withConnectors ] = await attachConnectorTypes ( [ visible ] )
0 commit comments