@@ -151,6 +151,19 @@ pub trait NotionApi: std::fmt::Debug + Send + Sync {
151151 let _ = start_cursor;
152152 Err ( LocalityError :: NotImplemented ( "search Notion databases" ) )
153153 }
154+ /// Search database metadata while allowing callers to bound provider-side
155+ /// work. Existing implementations remain source-compatible, but the
156+ /// default fails closed so setup cannot silently use an unbounded search.
157+ /// The HTTP client enforces the requested result bound before retrieving
158+ /// database metadata.
159+ fn search_databases_bounded (
160+ & self ,
161+ start_cursor : Option < & str > ,
162+ max_results : usize ,
163+ ) -> LocalityResult < DatabaseListDto > {
164+ let _ = ( start_cursor, max_results) ;
165+ Err ( LocalityError :: Unsupported ( "bounded Notion database search" ) )
166+ }
154167 fn update_block ( & self , block_id : & str , body : serde_json:: Value ) -> LocalityResult < BlockDto > ;
155168 fn move_block (
156169 & self ,
@@ -850,21 +863,25 @@ impl NotionApi for HttpNotionApi {
850863 }
851864
852865 fn search_databases ( & self , start_cursor : Option < & str > ) -> LocalityResult < DatabaseListDto > {
853- let data_sources: DataSourceListDto =
854- self . post_read_json ( "/v1/search" , data_source_search_body ( start_cursor) ) ?;
855- let mut seen = BTreeSet :: new ( ) ;
866+ self . search_databases_bounded ( start_cursor, 100 )
867+ }
868+
869+ fn search_databases_bounded (
870+ & self ,
871+ start_cursor : Option < & str > ,
872+ max_results : usize ,
873+ ) -> LocalityResult < DatabaseListDto > {
874+ if max_results == 0 {
875+ return Ok ( DatabaseListDto :: default ( ) ) ;
876+ }
877+ let max_results = max_results. min ( 100 ) ;
878+ let data_sources: DataSourceListDto = self . post_read_json (
879+ "/v1/search" ,
880+ data_source_search_body ( start_cursor, max_results) ,
881+ ) ?;
856882 let mut databases = Vec :: new ( ) ;
857- for data_source in data_sources. results {
858- let Some ( database_id) = data_source
859- . parent
860- . as_ref ( )
861- . and_then ( |parent| parent. database_id . as_deref ( ) )
862- else {
863- continue ;
864- } ;
865- if seen. insert ( database_id. to_string ( ) ) {
866- databases. push ( self . retrieve_database ( database_id) ?) ;
867- }
883+ for database_id in unique_database_ids ( & data_sources. results , max_results) {
884+ databases. push ( self . retrieve_database ( & database_id) ?) ;
868885 }
869886
870887 Ok ( DatabaseListDto {
@@ -931,9 +948,9 @@ impl NotionApi for HttpNotionApi {
931948 }
932949}
933950
934- fn data_source_search_body ( start_cursor : Option < & str > ) -> Value {
951+ fn data_source_search_body ( start_cursor : Option < & str > , page_size : usize ) -> Value {
935952 let mut body = json ! ( {
936- "page_size" : 100 ,
953+ "page_size" : page_size ,
937954 "filter" : {
938955 "property" : "object" ,
939956 "value" : "data_source"
@@ -951,13 +968,30 @@ fn data_source_search_body(start_cursor: Option<&str>) -> Value {
951968 body
952969}
953970
971+ fn unique_database_ids ( data_sources : & [ DataSourceDto ] , max_results : usize ) -> Vec < String > {
972+ let mut seen = BTreeSet :: new ( ) ;
973+ data_sources
974+ . iter ( )
975+ . filter_map ( |data_source| {
976+ data_source
977+ . parent
978+ . as_ref ( )
979+ . and_then ( |parent| parent. database_id . as_deref ( ) )
980+ } )
981+ . filter ( |database_id| seen. insert ( ( * database_id) . to_string ( ) ) )
982+ . take ( max_results)
983+ . map ( str:: to_string)
984+ . collect ( )
985+ }
986+
954987#[ cfg( test) ]
955988mod tests {
956989 use super :: {
957990 HttpNotionApi , NotionResponseInterpretation , NotionRetryClass , data_source_search_body,
958991 notion_http_client_builder, notion_network_config, notion_page_lookup_reports_database,
959- rate_limit_backoff, retry_after_header,
992+ rate_limit_backoff, retry_after_header, unique_database_ids ,
960993 } ;
994+ use crate :: dto:: { DataSourceDto , ParentDto } ;
961995 use locality_core:: LocalityError ;
962996 use reqwest:: header:: { HeaderMap , HeaderValue , RETRY_AFTER } ;
963997 use serde_json:: Value ;
@@ -972,11 +1006,39 @@ mod tests {
9721006
9731007 #[ test]
9741008 fn search_databases_uses_current_notion_data_source_filter ( ) {
975- let body = data_source_search_body ( Some ( "cursor-1" ) ) ;
1009+ let body = data_source_search_body ( Some ( "cursor-1" ) , 1 ) ;
9761010
9771011 assert_eq ! ( body[ "filter" ] [ "property" ] , "object" ) ;
9781012 assert_eq ! ( body[ "filter" ] [ "value" ] , "data_source" ) ;
9791013 assert_eq ! ( body[ "start_cursor" ] , "cursor-1" ) ;
1014+ assert_eq ! ( body[ "page_size" ] , 1 ) ;
1015+ }
1016+
1017+ #[ test]
1018+ fn bounded_database_search_selects_only_the_requested_unique_metadata_retrievals ( ) {
1019+ let data_sources = vec ! [
1020+ data_source( "source-1" , "database-1" ) ,
1021+ data_source( "source-2" , "database-1" ) ,
1022+ data_source( "source-3" , "database-2" ) ,
1023+ ] ;
1024+
1025+ assert_eq ! ( unique_database_ids( & data_sources, 1 ) , vec![ "database-1" ] ) ;
1026+ assert_eq ! (
1027+ unique_database_ids( & data_sources, 2 ) ,
1028+ vec![ "database-1" , "database-2" ]
1029+ ) ;
1030+ }
1031+
1032+ fn data_source ( id : & str , database_id : & str ) -> DataSourceDto {
1033+ DataSourceDto {
1034+ id : id. to_string ( ) ,
1035+ parent : Some ( ParentDto {
1036+ kind : "database_id" . to_string ( ) ,
1037+ database_id : Some ( database_id. to_string ( ) ) ,
1038+ ..Default :: default ( )
1039+ } ) ,
1040+ ..Default :: default ( )
1041+ }
9801042 }
9811043
9821044 #[ test]
0 commit comments