Skip to main content

allsource_core/infrastructure/query/
graphql.rs

1//! Lightweight GraphQL API for AllSource events and projections
2//!
3//! Implements a minimal GraphQL executor (no external dependencies) that supports:
4//! - Query `events(entity_id, event_type, limit)` → list of events
5//! - Query `event(id)` → single event by ID
6//! - Query `projections` → list projection names
7//! - Query `projection(name, entity_id)` → projection state
8//! - Query `stats` → store statistics
9//!
10//! ## Example query
11//!
12//! ```graphql
13//! {
14//!   events(event_type: "user.created", limit: 10) {
15//!     id
16//!     event_type
17//!     entity_id
18//!     payload
19//!     timestamp
20//!   }
21//! }
22//! ```
23//!
24//! ## Limitations
25//!
26//! - No mutations (read-only API)
27//! - No subscriptions (use WebSocket streaming instead)
28//! - No fragments, variables, or aliases
29//! - Field selection is advisory (all requested fields returned, unrequested fields omitted)
30
31use serde::{Deserialize, Serialize};
32use serde_json::Value as JsonValue;
33use std::collections::HashMap;
34
35/// GraphQL request body
36#[derive(Debug, Clone, Deserialize)]
37pub struct GraphQLRequest {
38    pub query: String,
39    #[serde(default)]
40    pub variables: Option<HashMap<String, JsonValue>>,
41}
42
43/// GraphQL response body
44#[derive(Debug, Clone, Serialize)]
45pub struct GraphQLResponse {
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub data: Option<JsonValue>,
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub errors: Vec<GraphQLError>,
50}
51
52/// A GraphQL error
53#[derive(Debug, Clone, Serialize)]
54pub struct GraphQLError {
55    pub message: String,
56}
57
58/// Parsed top-level query field
59#[derive(Debug, Clone)]
60pub struct QueryField {
61    pub name: String,
62    pub arguments: HashMap<String, String>,
63    pub fields: Vec<String>,
64}
65
66/// Parse a simple GraphQL query into query fields
67///
68/// Supports: `{ fieldName(arg: "value", arg2: "value2") { subfield1 subfield2 } }`
69pub fn parse_query(query: &str) -> Result<Vec<QueryField>, String> {
70    let trimmed = query.trim();
71
72    // Strip optional "query" keyword and find the outer braces
73    let body = if let Some(rest) = trimmed.strip_prefix("query") {
74        let rest = rest.trim();
75        // Skip optional query name
76        if let Some(idx) = rest.find('{') {
77            &rest[idx..]
78        } else {
79            return Err("Expected '{' after query keyword".to_string());
80        }
81    } else if trimmed.starts_with('{') {
82        trimmed
83    } else if trimmed.starts_with("mutation") {
84        return Err("Mutations are not supported (read-only API)".to_string());
85    } else if trimmed.starts_with("subscription") {
86        return Err("Subscriptions are not supported (use WebSocket streaming)".to_string());
87    } else {
88        return Err("Expected query to start with '{' or 'query'".to_string());
89    };
90
91    // Remove outer braces
92    let body = body
93        .strip_prefix('{')
94        .and_then(|b| b.trim().strip_suffix('}'))
95        .ok_or("Malformed query: missing outer braces")?
96        .trim();
97
98    if body.is_empty() {
99        return Err("Empty query body".to_string());
100    }
101
102    let mut fields = Vec::new();
103    let mut remaining = body;
104
105    while !remaining.is_empty() {
106        remaining = remaining.trim();
107        if remaining.is_empty() {
108            break;
109        }
110
111        // Extract field name
112        let name_end = remaining
113            .find(|c: char| c == '(' || c == '{' || c.is_whitespace())
114            .unwrap_or(remaining.len());
115        let name = remaining[..name_end].trim().to_string();
116        if name.is_empty() {
117            break;
118        }
119        remaining = remaining[name_end..].trim();
120
121        // Extract arguments if present
122        let mut arguments = HashMap::new();
123        if remaining.starts_with('(') {
124            let close = remaining.find(')').ok_or("Unclosed argument list")?;
125            let args_str = &remaining[1..close];
126            for arg in args_str.split(',') {
127                let arg = arg.trim();
128                if let Some((key, val)) = arg.split_once(':') {
129                    let key = key.trim().to_string();
130                    let val = val.trim().trim_matches('"').to_string();
131                    arguments.insert(key, val);
132                }
133            }
134            remaining = remaining[close + 1..].trim();
135        }
136
137        // Extract sub-fields if present
138        let mut sub_fields = Vec::new();
139        if remaining.starts_with('{') {
140            let close = find_matching_brace(remaining).ok_or("Unclosed field selection")?;
141            let fields_str = &remaining[1..close];
142            for field in fields_str.split_whitespace() {
143                let field = field.trim();
144                if !field.is_empty() {
145                    sub_fields.push(field.to_string());
146                }
147            }
148            remaining = remaining[close + 1..].trim();
149        }
150
151        fields.push(QueryField {
152            name,
153            arguments,
154            fields: sub_fields,
155        });
156    }
157
158    if fields.is_empty() {
159        return Err("No fields found in query".to_string());
160    }
161
162    Ok(fields)
163}
164
165/// Find the matching closing brace for the opening brace at position 0
166fn find_matching_brace(s: &str) -> Option<usize> {
167    let mut depth = 0;
168    for (i, c) in s.chars().enumerate() {
169        match c {
170            '{' => depth += 1,
171            '}' => {
172                depth -= 1;
173                if depth == 0 {
174                    return Some(i);
175                }
176            }
177            _ => {}
178        }
179    }
180    None
181}
182
183/// Convert an Event to a filtered JSON object based on requested fields
184pub fn event_to_json(event: &crate::domain::entities::Event, fields: &[String]) -> JsonValue {
185    let mut map = serde_json::Map::new();
186
187    let all_fields = fields.is_empty();
188
189    if all_fields || fields.iter().any(|f| f == "id") {
190        map.insert("id".to_string(), JsonValue::String(event.id.to_string()));
191    }
192    if all_fields || fields.iter().any(|f| f == "event_type") {
193        map.insert(
194            "event_type".to_string(),
195            JsonValue::String(event.event_type_str().to_string()),
196        );
197    }
198    if all_fields || fields.iter().any(|f| f == "entity_id") {
199        map.insert(
200            "entity_id".to_string(),
201            JsonValue::String(event.entity_id_str().to_string()),
202        );
203    }
204    if all_fields || fields.iter().any(|f| f == "tenant_id") {
205        map.insert(
206            "tenant_id".to_string(),
207            JsonValue::String(event.tenant_id_str().to_string()),
208        );
209    }
210    if all_fields || fields.iter().any(|f| f == "payload") {
211        map.insert("payload".to_string(), event.payload.clone());
212    }
213    if all_fields || fields.iter().any(|f| f == "metadata") {
214        map.insert(
215            "metadata".to_string(),
216            event.metadata.clone().unwrap_or(JsonValue::Null),
217        );
218    }
219    if all_fields || fields.iter().any(|f| f == "timestamp") {
220        map.insert(
221            "timestamp".to_string(),
222            JsonValue::String(event.timestamp.to_rfc3339()),
223        );
224    }
225    if all_fields || fields.iter().any(|f| f == "version") {
226        map.insert("version".to_string(), serde_json::json!(event.version));
227    }
228
229    JsonValue::Object(map)
230}
231
232/// Introspection schema response for `__schema` queries
233pub fn introspection_schema() -> JsonValue {
234    serde_json::json!({
235        "queryType": { "name": "Query" },
236        "types": [
237            {
238                "name": "Event",
239                "fields": [
240                    { "name": "id", "type": "String!" },
241                    { "name": "event_type", "type": "String!" },
242                    { "name": "entity_id", "type": "String!" },
243                    { "name": "tenant_id", "type": "String!" },
244                    { "name": "payload", "type": "JSON!" },
245                    { "name": "metadata", "type": "JSON" },
246                    { "name": "timestamp", "type": "DateTime!" },
247                    { "name": "version", "type": "Int!" }
248                ]
249            },
250            {
251                "name": "Query",
252                "fields": [
253                    { "name": "events", "args": ["entity_id", "event_type", "tenant_id", "limit"], "type": "[Event!]!" },
254                    { "name": "event", "args": ["id"], "type": "Event" },
255                    { "name": "projections", "type": "[String!]!" },
256                    { "name": "projection", "args": ["name", "entity_id"], "type": "JSON" },
257                    { "name": "stats", "type": "JSON!" }
258                ]
259            }
260        ]
261    })
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_parse_simple_query() {
270        let q = r"{ events { id event_type } }";
271        let fields = parse_query(q).unwrap();
272        assert_eq!(fields.len(), 1);
273        assert_eq!(fields[0].name, "events");
274        assert_eq!(fields[0].fields, vec!["id", "event_type"]);
275    }
276
277    #[test]
278    fn test_parse_with_arguments() {
279        let q = r#"{ events(event_type: "user.created", limit: "10") { id entity_id } }"#;
280        let fields = parse_query(q).unwrap();
281        assert_eq!(
282            fields[0].arguments.get("event_type").unwrap(),
283            "user.created"
284        );
285        assert_eq!(fields[0].arguments.get("limit").unwrap(), "10");
286    }
287
288    #[test]
289    fn test_parse_multiple_fields() {
290        let q = r"{ events { id } stats }";
291        let fields = parse_query(q).unwrap();
292        assert_eq!(fields.len(), 2);
293        assert_eq!(fields[0].name, "events");
294        assert_eq!(fields[1].name, "stats");
295    }
296
297    #[test]
298    fn test_parse_query_keyword() {
299        let q = r"query { events { id } }";
300        let fields = parse_query(q).unwrap();
301        assert_eq!(fields.len(), 1);
302        assert_eq!(fields[0].name, "events");
303    }
304
305    #[test]
306    fn test_reject_mutation() {
307        let q = r#"mutation { createEvent(type: "test") { id } }"#;
308        let result = parse_query(q);
309        assert!(result.is_err());
310        assert!(result.unwrap_err().contains("Mutations"));
311    }
312
313    #[test]
314    fn test_reject_subscription() {
315        let q = r"subscription { newEvents { id } }";
316        let result = parse_query(q);
317        assert!(result.is_err());
318        assert!(result.unwrap_err().contains("Subscriptions"));
319    }
320
321    #[test]
322    fn test_empty_query() {
323        let q = r"{ }";
324        let result = parse_query(q);
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn test_event_to_json_all_fields() {
330        let event = crate::domain::entities::Event::from_strings(
331            "user.created".to_string(),
332            "user-1".to_string(),
333            "default".to_string(),
334            serde_json::json!({"name": "Alice"}),
335            None,
336        )
337        .unwrap();
338        let json = event_to_json(&event, &[]);
339        assert!(json.get("id").is_some());
340        assert!(json.get("event_type").is_some());
341        assert!(json.get("payload").is_some());
342    }
343
344    #[test]
345    fn test_event_to_json_selected_fields() {
346        let event = crate::domain::entities::Event::from_strings(
347            "user.created".to_string(),
348            "user-1".to_string(),
349            "default".to_string(),
350            serde_json::json!({"name": "Alice"}),
351            None,
352        )
353        .unwrap();
354        let json = event_to_json(&event, &["id".to_string(), "event_type".to_string()]);
355        assert!(json.get("id").is_some());
356        assert!(json.get("event_type").is_some());
357        assert!(json.get("payload").is_none());
358    }
359
360    #[test]
361    fn test_introspection_schema() {
362        let schema = introspection_schema();
363        assert!(schema.get("queryType").is_some());
364        assert!(schema.get("types").is_some());
365    }
366}