mockforge_recorder/
query.rs

1//! Query API for recorded requests
2
3use crate::{database::RecorderDatabase, models::*, Result};
4use serde::{Deserialize, Serialize};
5
6/// Query filter for searching recorded requests
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct QueryFilter {
9    /// Filter by protocol
10    pub protocol: Option<Protocol>,
11    /// Filter by HTTP method or gRPC method
12    pub method: Option<String>,
13    /// Filter by path (supports wildcards)
14    pub path: Option<String>,
15    /// Filter by status code
16    pub status_code: Option<i32>,
17    /// Filter by trace ID
18    pub trace_id: Option<String>,
19    /// Filter by minimum duration (ms)
20    pub min_duration_ms: Option<i64>,
21    /// Filter by maximum duration (ms)
22    pub max_duration_ms: Option<i64>,
23    /// Filter by tags
24    pub tags: Option<Vec<String>>,
25    /// Limit number of results
26    pub limit: Option<i32>,
27    /// Offset for pagination
28    pub offset: Option<i32>,
29}
30
31/// Query result
32#[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
40/// Execute a query against the database
41pub async fn execute_query(db: &RecorderDatabase, filter: QueryFilter) -> Result<QueryResult> {
42    // Build SQL query based on filters
43    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    // Add filters
55    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    // Order by timestamp descending
96    query.push_str(" ORDER BY timestamp DESC");
97
98    // Add limit and offset
99    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    // For now, use the list_recent method as a placeholder
104    // Full query implementation would require dynamic query building
105    let requests = db.list_recent(limit).await?;
106
107    // Fetch responses for each request
108    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}