diff --git a/apps/fortuna/src/api/explorer.rs b/apps/fortuna/src/api/explorer.rs index 7e643d589a..d682dc4ead 100644 --- a/apps/fortuna/src/api/explorer.rs +++ b/apps/fortuna/src/api/explorer.rs @@ -35,6 +35,12 @@ pub struct ExplorerQueryParams { pub offset: Option, } +#[derive(Debug, serde::Serialize, utoipa::ToSchema)] +pub struct ExplorerResponse { + pub requests: Vec, + pub total_results: u64, +} + /// Returns the logs of all requests captured by the keeper. /// /// This endpoint allows you to filter the logs by a specific network ID, a query string (which can be a transaction hash, sender address, or sequence number), and a time range. @@ -42,13 +48,13 @@ pub struct ExplorerQueryParams { #[utoipa::path( get, path = "/v1/logs", - responses((status = 200, description = "A list of Entropy request logs", body = Vec)), + responses((status = 200, description = "A list of Entropy request logs", body = ExplorerResponse)), params(ExplorerQueryParams) )] pub async fn explorer( State(state): State, Query(query_params): Query, -) -> anyhow::Result>, RestError> { +) -> anyhow::Result, RestError> { if let Some(network_id) = &query_params.network_id { if !state .chains @@ -89,10 +95,13 @@ pub async fn explorer( if let Some(max_timestamp) = query_params.max_timestamp { query = query.max_timestamp(max_timestamp); } - Ok(Json( - query - .execute() - .await - .map_err(|_| RestError::TemporarilyUnavailable)?, - )) + + let (requests, total_results) = tokio::join!(query.execute(), query.count_results()); + let requests = requests.map_err(|_| RestError::TemporarilyUnavailable)?; + let total_results = total_results.map_err(|_| RestError::TemporarilyUnavailable)?; + + Ok(Json(ExplorerResponse { + requests, + total_results, + })) } diff --git a/apps/fortuna/src/command/run.rs b/apps/fortuna/src/command/run.rs index 33a10c6ef1..704543a986 100644 --- a/apps/fortuna/src/command/run.rs +++ b/apps/fortuna/src/command/run.rs @@ -45,6 +45,7 @@ pub async fn run_api( crate::api::Blob, crate::api::BinaryEncoding, crate::api::StateTag, + crate::api::ExplorerResponse, ) ), tags( diff --git a/apps/fortuna/src/history.rs b/apps/fortuna/src/history.rs index e1160f21c1..4add2a648c 100644 --- a/apps/fortuna/src/history.rs +++ b/apps/fortuna/src/history.rs @@ -427,8 +427,34 @@ impl<'a> RequestQueryBuilder<'a> { } pub async fn execute(&self) -> Result> { - let mut query_builder = - QueryBuilder::new("SELECT * FROM request WHERE created_at BETWEEN "); + let mut query_builder = self.build_query("*"); + query_builder.push(" LIMIT "); + query_builder.push_bind(self.limit); + query_builder.push(" OFFSET "); + query_builder.push_bind(self.offset); + + let result: sqlx::Result> = + query_builder.build_query_as().fetch_all(self.pool).await; + + if let Err(e) = &result { + tracing::error!("Failed to fetch request: {}", e); + } + + Ok(result?.into_iter().filter_map(|row| row.into()).collect()) + } + + pub async fn count_results(&self) -> Result { + self.build_query("COUNT(*) AS count") + .build_query_scalar::() + .fetch_one(self.pool) + .await + .map_err(|err| err.into()) + } + + fn build_query(&self, columns: &str) -> QueryBuilder { + let mut query_builder = QueryBuilder::new(format!( + "SELECT {columns} FROM request WHERE created_at BETWEEN " + )); query_builder.push_bind(self.min_timestamp); query_builder.push(" AND "); query_builder.push_bind(self.max_timestamp); @@ -464,21 +490,8 @@ impl<'a> RequestQueryBuilder<'a> { query_builder.push_bind(state); } - query_builder.push(" ORDER BY created_at DESC LIMIT "); - query_builder.push_bind(self.limit); - query_builder.push(" OFFSET "); - query_builder.push_bind(self.offset); - - let rows = query_builder - .build_query_as::() - .fetch_all(self.pool) - .await; - - if let Err(e) = &rows { - tracing::error!("Failed to fetch request by time: {}", e); - } - - Ok(rows?.into_iter().filter_map(|row| row.into()).collect()) + query_builder.push(" ORDER BY created_at DESC"); + query_builder } } @@ -928,4 +941,38 @@ mod test { .unwrap(); assert_eq!(logs, vec![status]); } + + #[tokio::test] + async fn test_count_results() { + let history = History::new_in_memory().await.unwrap(); + History::update_request_status(&history.pool, get_random_request_status()).await; + History::update_request_status(&history.pool, get_random_request_status()).await; + let mut failed_status = get_random_request_status(); + History::update_request_status(&history.pool, failed_status.clone()).await; + failed_status.state = RequestEntryState::Failed { + reason: "Failed".to_string(), + provider_random_number: None, + }; + History::update_request_status(&history.pool, failed_status.clone()).await; + + let results = history.query().count_results().await.unwrap(); + assert_eq!(results, 3); + + let results = history + .query() + .limit(1) + .unwrap() + .count_results() + .await + .unwrap(); + assert_eq!(results, 3); + + let results = history + .query() + .state(StateTag::Pending) + .count_results() + .await + .unwrap(); + assert_eq!(results, 2); + } }