@@ -12,37 +12,45 @@ import { withOptionalAuthV2 } from "@/withAuthV2";
1212import * as grpc from '@grpc/grpc-js' ;
1313import * as protoLoader from '@grpc/proto-loader' ;
1414import * as Sentry from '@sentry/nextjs' ;
15- import { PrismaClient , Repo } from "@sourcebot/db" ;
16- import { createLogger , env } from "@sourcebot/shared" ;
15+ import { PrismaClient , Repo , UserWithAccounts } from "@sourcebot/db" ;
16+ import { createLogger , env , hasEntitlement } from "@sourcebot/shared" ;
1717import path from 'path' ;
1818import { parseQueryIntoLezerTree , transformLezerTreeToZoektGrpcQuery } from './query' ;
1919import { RepositoryInfo , SearchRequest , SearchResponse , SearchResultFile , SearchStats , SourceRange , StreamedSearchResponse } from "./types" ;
2020import { FlushReason as ZoektFlushReason } from "@/proto/zoekt/webserver/v1/FlushReason" ;
2121import { RevisionExpr } from "@sourcebot/query-language" ;
2222import { getCodeHostBrowseFileAtBranchUrl } from "@/lib/utils" ;
23+ import { getRepoPermissionFilterForUser } from "@/prisma" ;
2324
2425const logger = createLogger ( "searchApi" ) ;
2526
2627export const search = ( searchRequest : SearchRequest ) => sew ( ( ) =>
27- withOptionalAuthV2 ( async ( { prisma } ) => {
28+ withOptionalAuthV2 ( async ( { prisma, user } ) => {
29+ const repoSearchScope = await getAccessibleRepoNamesForUser ( { user, prisma } ) ;
30+
2831 const zoektSearchRequest = await createZoektSearchRequest ( {
2932 searchRequest,
3033 prisma,
34+ repoSearchScope,
3135 } ) ;
3236
33- logger . debug ( 'zoektSearchRequest:' , JSON . stringify ( zoektSearchRequest , null , 2 ) ) ;
37+
38+ logger . debug ( `zoektSearchRequest:\n${ JSON . stringify ( zoektSearchRequest , null , 2 ) } ` ) ;
3439
3540 return zoektSearch ( zoektSearchRequest , prisma ) ;
3641 } ) ) ;
3742
3843export const streamSearch = ( searchRequest : SearchRequest ) => sew ( ( ) =>
39- withOptionalAuthV2 ( async ( { prisma } ) => {
44+ withOptionalAuthV2 ( async ( { prisma, user } ) => {
45+ const repoSearchScope = await getAccessibleRepoNamesForUser ( { user, prisma } ) ;
46+
4047 const zoektSearchRequest = await createZoektSearchRequest ( {
4148 searchRequest,
4249 prisma,
50+ repoSearchScope,
4351 } ) ;
4452
45- logger . debug ( ' zoektStreamSearchRequest:' , JSON . stringify ( zoektSearchRequest , null , 2 ) ) ;
53+ console . log ( ` zoektStreamSearchRequest:\n ${ JSON . stringify ( zoektSearchRequest , null , 2 ) } ` ) ;
4654
4755 return zoektStreamSearch ( zoektSearchRequest , prisma ) ;
4856 } ) ) ;
@@ -296,9 +304,9 @@ const transformZoektSearchResponse = async (response: ZoektGrpcSearchResponse, r
296304 const repoId = getRepoIdForFile ( file ) ;
297305 const repo = reposMapCache . get ( repoId ) ;
298306
299- // This can happen if the user doesn't have access to the repository .
307+ // This should never happen .
300308 if ( ! repo ) {
301- return undefined ;
309+ throw new Error ( `Repository not found for file: ${ file . file_name } ` ) ;
302310 }
303311
304312 // @todo : address "file_name might not be a valid UTF-8 string" warning.
@@ -432,9 +440,12 @@ const getRepoIdForFile = (file: ZoektGrpcFileMatch): string | number => {
432440const createZoektSearchRequest = async ( {
433441 searchRequest,
434442 prisma,
443+ repoSearchScope,
435444} : {
436445 searchRequest : SearchRequest ;
437446 prisma : PrismaClient ;
447+ // Allows the caller to scope the search to a specific set of repositories.
448+ repoSearchScope ?: string [ ] ;
438449} ) => {
439450 const tree = parseQueryIntoLezerTree ( searchRequest . query ) ;
440451 const zoektQuery = await transformLezerTreeToZoektGrpcQuery ( {
@@ -487,6 +498,14 @@ const createZoektSearchRequest = async ({
487498 exact : true ,
488499 }
489500 } ] : [ ] ) ,
501+ ...( repoSearchScope ? [ {
502+ repo_set : {
503+ set : repoSearchScope . reduce ( ( acc , repo ) => {
504+ acc [ repo ] = true ;
505+ return acc ;
506+ } , { } as Record < string , boolean > )
507+ }
508+ } ] : [ ] ) ,
490509 ]
491510 }
492511 } ,
@@ -542,6 +561,27 @@ const createZoektSearchRequest = async ({
542561 return zoektSearchRequest ;
543562}
544563
564+ /**
565+ * Returns a list of repository names that the user has access to.
566+ * If permission syncing is disabled, returns undefined.
567+ */
568+ const getAccessibleRepoNamesForUser = async ( { user, prisma } : { user ?: UserWithAccounts , prisma : PrismaClient } ) => {
569+ if (
570+ env . EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true' ||
571+ ! hasEntitlement ( 'permission-syncing' )
572+ ) {
573+ return undefined ;
574+ }
575+
576+ const accessibleRepos = await prisma . repo . findMany ( {
577+ where : getRepoPermissionFilterForUser ( user ) ,
578+ select : {
579+ name : true ,
580+ }
581+ } ) ;
582+ return accessibleRepos . map ( repo => repo . name ) ;
583+ }
584+
545585const createGrpcClient = ( ) : WebserverServiceClient => {
546586 // Path to proto files - these should match your monorepo structure
547587 const protoBasePath = path . join ( process . cwd ( ) , '../../vendor/zoekt/grpc/protos' ) ;
0 commit comments