allsource_core/infrastructure/query/
graphql.rs1use serde::{Deserialize, Serialize};
32use serde_json::Value as JsonValue;
33use std::collections::HashMap;
34
35#[derive(Debug, Clone, Deserialize)]
37pub struct GraphQLRequest {
38 pub query: String,
39 #[serde(default)]
40 pub variables: Option<HashMap<String, JsonValue>>,
41}
42
43#[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#[derive(Debug, Clone, Serialize)]
54pub struct GraphQLError {
55 pub message: String,
56}
57
58#[derive(Debug, Clone)]
60pub struct QueryField {
61 pub name: String,
62 pub arguments: HashMap<String, String>,
63 pub fields: Vec<String>,
64}
65
66pub fn parse_query(query: &str) -> Result<Vec<QueryField>, String> {
70 let trimmed = query.trim();
71
72 let body = if let Some(rest) = trimmed.strip_prefix("query") {
74 let rest = rest.trim();
75 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 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 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 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 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
165fn 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
183pub 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
232pub 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}