mockforge_recorder/
query.rs1use crate::{database::RecorderDatabase, models::*, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct QueryFilter {
9 pub protocol: Option<Protocol>,
11 pub method: Option<String>,
13 pub path: Option<String>,
15 pub status_code: Option<i32>,
17 pub trace_id: Option<String>,
19 pub min_duration_ms: Option<i64>,
21 pub max_duration_ms: Option<i64>,
23 pub tags: Option<Vec<String>>,
25 pub limit: Option<i32>,
27 pub offset: Option<i32>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct QueryResult {
34 pub total: i64,
35 pub offset: i32,
36 pub limit: i32,
37 pub exchanges: Vec<RecordedExchange>,
38}
39
40pub async fn execute_query(db: &RecorderDatabase, filter: QueryFilter) -> Result<QueryResult> {
42 let mut query = String::from(
44 r#"
45 SELECT id, protocol, timestamp, method, path, query_params,
46 headers, body, body_encoding, client_ip, trace_id, span_id,
47 duration_ms, status_code, tags
48 FROM requests WHERE 1=1
49 "#,
50 );
51
52 let mut params: Vec<Box<dyn sqlx::Encode<'_, sqlx::Sqlite> + Send>> = Vec::new();
53
54 if let Some(protocol) = filter.protocol {
56 query.push_str(" AND protocol = ?");
57 params.push(Box::new(protocol.as_str().to_string()));
58 }
59
60 if let Some(method) = &filter.method {
61 query.push_str(" AND method = ?");
62 params.push(Box::new(method.clone()));
63 }
64
65 if let Some(path) = &filter.path {
66 if path.contains('*') {
67 query.push_str(" AND path LIKE ?");
68 params.push(Box::new(path.replace('*', "%")));
69 } else {
70 query.push_str(" AND path = ?");
71 params.push(Box::new(path.clone()));
72 }
73 }
74
75 if let Some(status) = filter.status_code {
76 query.push_str(" AND status_code = ?");
77 params.push(Box::new(status));
78 }
79
80 if let Some(trace_id) = &filter.trace_id {
81 query.push_str(" AND trace_id = ?");
82 params.push(Box::new(trace_id.clone()));
83 }
84
85 if let Some(min_duration) = filter.min_duration_ms {
86 query.push_str(" AND duration_ms >= ?");
87 params.push(Box::new(min_duration));
88 }
89
90 if let Some(max_duration) = filter.max_duration_ms {
91 query.push_str(" AND duration_ms <= ?");
92 params.push(Box::new(max_duration));
93 }
94
95 query.push_str(" ORDER BY timestamp DESC");
97
98 let limit = filter.limit.unwrap_or(100);
100 let offset = filter.offset.unwrap_or(0);
101 query.push_str(&format!(" LIMIT {} OFFSET {}", limit, offset));
102
103 let requests = db.list_recent(limit).await?;
106
107 let mut exchanges = Vec::new();
109 for request in requests {
110 let response = db.get_response(&request.id).await?;
111 exchanges.push(RecordedExchange { request, response });
112 }
113
114 Ok(QueryResult {
115 total: exchanges.len() as i64,
116 offset,
117 limit,
118 exchanges,
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_query_filter_creation() {
128 let filter = QueryFilter {
129 protocol: Some(Protocol::Http),
130 method: Some("GET".to_string()),
131 path: Some("/api/*".to_string()),
132 ..Default::default()
133 };
134
135 assert_eq!(filter.protocol, Some(Protocol::Http));
136 assert_eq!(filter.method, Some("GET".to_string()));
137 }
138}