Skip to main content

sz_orm_graphql/
lib.rs

1//! # SZ-ORM GraphQL — GraphQL Schema 解析与查询
2//!
3//! 提供 GraphQL Schema 定义、类型/字段/查询/变更构建与查询执行,
4//! 启用 `real` feature 后接入真实 GraphQL 引擎。
5//!
6//! ## 主要类型
7//!
8//! - [`GraphQLSchema`] — Schema 容器
9//! - [`GraphQLType`] / [`GraphQLField`] — 类型与字段定义
10//! - [`resolver::DbResolver`] — 真实 DB resolver trait(P2-1 修复 C-3:可注入真实数据源)
11//!
12//! ## P2-1 修复 C-3:DB resolver 注入
13//!
14//! `GraphQLServer::with_db_resolver` 允许调用方注入真实 DB resolver,
15//! 启用后 GraphQL root field 通过 resolver 异步查询真实数据库,
16//! 未注入时回退到 mock 数据(向后兼容)。
17
18pub mod extensions;
19pub mod resolver;
20
21#[cfg(feature = "graphql-complexity")]
22pub mod complexity;
23#[cfg(feature = "graphql-n1")]
24pub mod dataloader;
25#[cfg(any(feature = "graphql-n1", feature = "graphql-complexity"))]
26pub mod query_ir;
27#[cfg(feature = "graphql-schema-gen")]
28pub mod schema_gen;
29
30use serde::{Deserialize, Serialize};
31
32#[cfg(feature = "real")]
33mod real_graphql;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct GraphQLSchema {
37    pub types: Vec<GraphQLType>,
38    pub queries: Vec<GraphQLField>,
39    pub mutations: Vec<GraphQLField>,
40}
41
42impl GraphQLSchema {
43    pub fn new() -> Self {
44        Self {
45            types: vec![],
46            queries: vec![],
47            mutations: vec![],
48        }
49    }
50
51    pub fn add_type(mut self, t: GraphQLType) -> Self {
52        self.types.push(t);
53        self
54    }
55
56    pub fn add_query(mut self, f: GraphQLField) -> Self {
57        self.queries.push(f);
58        self
59    }
60
61    pub fn add_mutation(mut self, f: GraphQLField) -> Self {
62        self.mutations.push(f);
63        self
64    }
65
66    /// Render the schema to a GraphQL SDL string.
67    pub fn to_sdl(&self) -> String {
68        let mut out = String::new();
69        for t in &self.types {
70            out.push_str(&format!("type {} {{\n", t.name));
71            for f in &t.fields {
72                out.push_str(&format!("    {}: {}\n", f.name, f.type_name));
73            }
74            out.push_str("}\n\n");
75        }
76        out.push_str("type Query {\n");
77        for q in &self.queries {
78            out.push_str(&format!("    {}: {}\n", q.name, q.type_name));
79        }
80        out.push_str("}\n");
81        if !self.mutations.is_empty() {
82            out.push_str("\ntype Mutation {\n");
83            for m in &self.mutations {
84                out.push_str(&format!("    {}: {}\n", m.name, m.type_name));
85            }
86            out.push_str("}\n");
87        }
88        out
89    }
90}
91
92impl Default for GraphQLSchema {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct GraphQLType {
100    pub name: String,
101    pub fields: Vec<GraphQLField>,
102}
103
104impl GraphQLType {
105    pub fn new(name: &str) -> Self {
106        Self {
107            name: name.to_string(),
108            fields: vec![],
109        }
110    }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct GraphQLField {
115    pub name: String,
116    pub type_name: String,
117}
118
119/// Convert a model name like "users" or "order_items" to a PascalCase singular
120/// GraphQL type name like "User" / "OrderItem".
121fn to_pascal_singular(input: &str) -> String {
122    let mut out = String::new();
123    let mut cap_next = true;
124    for ch in input.chars() {
125        if ch == '_' || ch == '-' || ch == ' ' {
126            cap_next = true;
127        } else if cap_next {
128            out.extend(ch.to_uppercase());
129            cap_next = false;
130        } else {
131            out.push(ch);
132        }
133    }
134    let len = out.len();
135    if len > 1 && out.ends_with('s') && !out.ends_with("ss") {
136        out.truncate(len - 1);
137    }
138    out
139}
140
141pub struct GraphQLSchemaGenerator;
142
143impl GraphQLSchemaGenerator {
144    /// Generate a real GraphQL schema from model names: for each model, emit a
145    /// `type` definition with id/name/createdAt/updatedAt fields plus a
146    /// `getX` query and a `listXs` query.
147    pub fn generate_schema(models: &[&str]) -> GraphQLSchema {
148        let mut schema = GraphQLSchema::new();
149        for m in models {
150            let type_name = to_pascal_singular(m);
151            let mut t = GraphQLType::new(&type_name);
152            t.fields.push(GraphQLField {
153                name: "id".to_string(),
154                type_name: "ID!".to_string(),
155            });
156            t.fields.push(GraphQLField {
157                name: "name".to_string(),
158                type_name: "String!".to_string(),
159            });
160            t.fields.push(GraphQLField {
161                name: "createdAt".to_string(),
162                type_name: "String!".to_string(),
163            });
164            t.fields.push(GraphQLField {
165                name: "updatedAt".to_string(),
166                type_name: "String!".to_string(),
167            });
168            schema = schema.add_type(t);
169            schema = schema.add_query(GraphQLField {
170                name: format!("get{}", type_name),
171                type_name: type_name.clone(),
172            });
173            schema = schema.add_query(GraphQLField {
174                name: format!("list{}s", type_name),
175                type_name: format!("[{}!]!", type_name),
176            });
177        }
178        schema
179    }
180}
181
182pub struct GraphQLServer {
183    port: u16,
184    schema: Option<GraphQLSchema>,
185    #[cfg(feature = "real")]
186    dynamic_schema: std::sync::OnceLock<Result<async_graphql::dynamic::Schema, String>>,
187    #[cfg(feature = "real")]
188    resolver: Option<resolver::SharedDbResolver>,
189}
190
191impl GraphQLServer {
192    pub fn new(port: u16) -> Self {
193        Self {
194            port,
195            schema: None,
196            #[cfg(feature = "real")]
197            dynamic_schema: std::sync::OnceLock::new(),
198            #[cfg(feature = "real")]
199            resolver: None,
200        }
201    }
202
203    pub fn with_schema(mut self, s: GraphQLSchema) -> Self {
204        self.schema = Some(s);
205        self
206    }
207
208    /// 注入真实 DB resolver — P2-1 修复 C-3
209    ///
210    /// 启用后,GraphQL root field 将通过 resolver 查询真实数据库,
211    /// 而非返回 mock 数据。
212    #[cfg(feature = "real")]
213    pub fn with_db_resolver(mut self, resolver: resolver::SharedDbResolver) -> Self {
214        self.resolver = Some(resolver);
215        self
216    }
217
218    /// Start a background tokio task that binds a TCP listener on the port.
219    /// Returns the URL the server is listening on.
220    /// Must be called from within a tokio runtime context.
221    #[cfg(not(feature = "real"))]
222    pub fn start(&self) -> Result<String, String> {
223        if self.schema.is_none() {
224            return Err("No schema".to_string());
225        }
226        let port = self.port;
227        let url = format!("http://localhost:{}", port);
228        // Spawn a background task that occupies the port with a TCP listener.
229        tokio::spawn(async move {
230            let addr = format!("127.0.0.1:{}", port);
231            match tokio::net::TcpListener::bind(&addr).await {
232                Ok(listener) => {
233                    while listener.accept().await.is_ok() {
234                        // Accept and drop; this is a placeholder server.
235                    }
236                }
237                Err(_) => {
238                    // Port may already be in use; the spawn task just exits.
239                }
240            }
241        });
242        Ok(url)
243    }
244
245    /// Lazily build (and cache) the executable async-graphql schema.
246    #[cfg(feature = "real")]
247    fn executable_schema(&self) -> Result<&async_graphql::dynamic::Schema, String> {
248        let schema = self.schema.as_ref().ok_or("No schema")?;
249        self.dynamic_schema
250            .get_or_init(|| real_graphql::build_dynamic_schema(schema, self.resolver.as_ref()))
251            .as_ref()
252            .map_err(Clone::clone)
253    }
254
255    /// Start a background tokio task that serves real GraphQL over HTTP via
256    /// axum + async-graphql (`POST /graphql`). Returns the URL the server is
257    /// listening on. Must be called from within a tokio runtime context.
258    #[cfg(feature = "real")]
259    pub fn start(&self) -> Result<String, String> {
260        let schema = self.executable_schema()?.clone();
261        let port = self.port;
262        let url = format!("http://localhost:{}", port);
263        tokio::spawn(async move {
264            let addr = format!("127.0.0.1:{}", port);
265            match tokio::net::TcpListener::bind(&addr).await {
266                Ok(listener) => {
267                    let _ = axum::serve(listener, real_graphql::router(schema)).await;
268                }
269                Err(_) => {
270                    // Port may already be in use; the spawn task just exits.
271                }
272            }
273        });
274        Ok(url)
275    }
276
277    /// Execute a simple GraphQL query of the form `{ getX(id: 1) { id name } }`
278    /// or `{ listXs { id name } }`. Returns mock JSON data based on the schema.
279    #[cfg(not(feature = "real"))]
280    pub fn execute_query(&self, query: &str) -> Result<serde_json::Value, String> {
281        let schema = self.schema.as_ref().ok_or("No schema")?;
282        let trimmed = query.trim();
283        let brace_start = trimmed.find('{').ok_or("Missing '{' in query")?;
284        let after_brace = trimmed[brace_start + 1..].trim_start();
285        // The query name ends at the first whitespace or '('.
286        let end = after_brace
287            .find(|c: char| c.is_whitespace() || c == '(' || c == '{')
288            .unwrap_or(after_brace.len());
289        let query_name = after_brace[..end].trim();
290        if query_name.is_empty() {
291            return Err("Empty query name".to_string());
292        }
293        let field = schema
294            .queries
295            .iter()
296            .find(|q| q.name == query_name)
297            .ok_or_else(|| format!("Query '{}' not found in schema", query_name))?;
298        if field.type_name.starts_with('[') {
299            // List query: return an array of mock objects.
300            Ok(serde_json::json!([
301                {
302                    "id": "1",
303                    "name": format!("{}_1", field.name),
304                    "createdAt": "2024-01-01T00:00:00Z",
305                    "updatedAt": "2024-01-01T00:00:00Z"
306                },
307                {
308                    "id": "2",
309                    "name": format!("{}_2", field.name),
310                    "createdAt": "2024-01-01T00:00:00Z",
311                    "updatedAt": "2024-01-01T00:00:00Z"
312                }
313            ]))
314        } else {
315            // Single query: return one mock object.
316            Ok(serde_json::json!({
317                "id": "1",
318                "name": format!("{}_1", field.name),
319                "createdAt": "2024-01-01T00:00:00Z",
320                "updatedAt": "2024-01-01T00:00:00Z"
321            }))
322        }
323    }
324
325    /// Execute a GraphQL query with the real async-graphql engine and return
326    /// the resolved value of the first root field as JSON.
327    #[cfg(feature = "real")]
328    pub fn execute_query(&self, query: &str) -> Result<serde_json::Value, String> {
329        let schema = self.executable_schema()?;
330        real_graphql::execute(schema, query)
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_schema_new() {
340        let s = GraphQLSchema::new();
341        assert!(s.types.is_empty());
342    }
343
344    #[test]
345    fn test_schema_add_type() {
346        let s = GraphQLSchema::new().add_type(GraphQLType::new("User"));
347        assert_eq!(s.types.len(), 1);
348    }
349
350    #[test]
351    fn test_generator_creates_types_and_queries() {
352        let s = GraphQLSchemaGenerator::generate_schema(&["users", "orders"]);
353        // 2 model types
354        assert_eq!(s.types.len(), 2);
355        // 2 queries per model (getX + listXs) = 4 queries
356        assert_eq!(s.queries.len(), 4);
357        // Verify each type has the required fields
358        for t in &s.types {
359            assert!(t
360                .fields
361                .iter()
362                .any(|f| f.name == "id" && f.type_name == "ID!"));
363            assert!(t.fields.iter().any(|f| f.name == "name"));
364            assert!(t.fields.iter().any(|f| f.name == "createdAt"));
365            assert!(t.fields.iter().any(|f| f.name == "updatedAt"));
366        }
367        // Verify both getX and listXs queries exist for "users" -> "User"
368        assert!(s
369            .queries
370            .iter()
371            .any(|q| q.name == "getUser" && q.type_name == "User"));
372        assert!(s
373            .queries
374            .iter()
375            .any(|q| q.name == "listUsers" && q.type_name == "[User!]!"));
376        assert!(s
377            .queries
378            .iter()
379            .any(|q| q.name == "getOrder" && q.type_name == "Order"));
380        assert!(s
381            .queries
382            .iter()
383            .any(|q| q.name == "listOrders" && q.type_name == "[Order!]!"));
384    }
385
386    #[test]
387    fn test_schema_sdl_contains_all_models() {
388        let s = GraphQLSchemaGenerator::generate_schema(&["users", "orders"]);
389        let sdl = s.to_sdl();
390        assert!(sdl.contains("type User {"));
391        assert!(sdl.contains("type Order {"));
392        assert!(sdl.contains("type Query {"));
393        assert!(sdl.contains("getUser: User"));
394        assert!(sdl.contains("listUsers: [User!]!"));
395    }
396
397    #[test]
398    fn test_server_new() {
399        let srv = GraphQLServer::new(4000);
400        assert_eq!(srv.port, 4000);
401    }
402
403    #[test]
404    fn test_server_start_without_schema_fails() {
405        let srv = GraphQLServer::new(4000);
406        assert!(srv.start().is_err());
407    }
408
409    #[tokio::test]
410    async fn test_server_start_returns_url_and_binds_port() {
411        let srv = GraphQLServer::new(4123)
412            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
413        let url = srv.start().expect("start should succeed");
414        assert!(url.contains("4123"));
415        // Give the spawned task a moment to bind the port.
416        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
417        // Verify the port is now bound by trying to bind to it again (should fail).
418        let second = tokio::net::TcpListener::bind("127.0.0.1:4123").await;
419        assert!(
420            second.is_err(),
421            "Port 4123 should already be bound by the spawned server task"
422        );
423    }
424
425    #[test]
426    fn test_execute_query_single() {
427        let srv = GraphQLServer::new(4001)
428            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
429        let result = srv.execute_query("{ getUser(id: 1) { id name } }");
430        assert!(result.is_ok(), "expected ok, got {:?}", result);
431        let v = result.unwrap();
432        assert_eq!(v["id"], "1");
433        assert!(v["name"].as_str().unwrap().contains("getUser"));
434    }
435
436    #[test]
437    fn test_execute_query_list() {
438        let srv = GraphQLServer::new(4002)
439            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
440        let result = srv.execute_query("{ listUsers { id name } }");
441        assert!(result.is_ok(), "expected ok, got {:?}", result);
442        let v = result.unwrap();
443        assert!(v.is_array());
444        assert_eq!(v.as_array().unwrap().len(), 2);
445    }
446
447    #[test]
448    fn test_execute_query_unknown_returns_error() {
449        let srv = GraphQLServer::new(4003)
450            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
451        let result = srv.execute_query("{ unknownQuery { id } }");
452        assert!(result.is_err());
453        assert!(result.unwrap_err().contains("unknownQuery"));
454    }
455
456    #[test]
457    fn test_execute_query_without_schema_fails() {
458        let srv = GraphQLServer::new(4004);
459        let result = srv.execute_query("{ getUser { id } }");
460        assert!(result.is_err());
461    }
462
463    #[test]
464    fn test_execute_query_malformed_no_brace() {
465        let srv = GraphQLServer::new(4005)
466            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
467        let result = srv.execute_query("getUser");
468        assert!(result.is_err());
469    }
470
471    /// Send a real GraphQL POST request over HTTP/1.0 and return the status
472    /// code together with the decoded JSON body. HTTP/1.0 keeps the response
473    /// free of chunked transfer encoding, so the body ends at connection
474    /// close.
475    #[cfg(feature = "real")]
476    async fn post_graphql(url: &str, body: &str) -> (u16, serde_json::Value) {
477        use tokio::io::{AsyncReadExt, AsyncWriteExt};
478        let without_scheme = url
479            .strip_prefix("http://")
480            .expect("url must start with http://");
481        let (addr, path) = without_scheme
482            .split_once('/')
483            .unwrap_or((without_scheme, ""));
484        let mut stream = tokio::net::TcpStream::connect(addr)
485            .await
486            .expect("connect should succeed");
487        let request = format!(
488            "POST /{path} HTTP/1.0\r\nHost: {addr}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
489            body.len()
490        );
491        stream
492            .write_all(request.as_bytes())
493            .await
494            .expect("write should succeed");
495        let mut raw = Vec::new();
496        stream
497            .read_to_end(&mut raw)
498            .await
499            .expect("read should succeed");
500        let text = String::from_utf8(raw).expect("response must be valid UTF-8");
501        let (head, body) = text
502            .split_once("\r\n\r\n")
503            .expect("response must contain a header/body separator");
504        let status = head
505            .split_whitespace()
506            .nth(1)
507            .and_then(|code| code.parse::<u16>().ok())
508            .expect("status line must contain a numeric status code");
509        let json = serde_json::from_str(body).expect("response body must be valid JSON");
510        (status, json)
511    }
512
513    #[cfg(feature = "real")]
514    #[tokio::test]
515    #[ignore = "requires the real GraphQL server (feature `real`)"]
516    async fn test_real_http_post_single_query() {
517        let srv = GraphQLServer::new(4331)
518            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
519        let url = srv.start().expect("start should succeed");
520        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
521        let (status, body) = post_graphql(
522            &format!("{url}/graphql"),
523            &serde_json::json!({"query": "{ getUser(id: 1) { id name } }"}).to_string(),
524        )
525        .await;
526        assert_eq!(status, 200);
527        assert_eq!(body["data"]["getUser"]["id"], "1");
528        assert!(body["data"]["getUser"]["name"]
529            .as_str()
530            .expect("name must be a string")
531            .contains("getUser"));
532    }
533
534    #[cfg(feature = "real")]
535    #[tokio::test]
536    #[ignore = "requires the real GraphQL server (feature `real`)"]
537    async fn test_real_http_post_list_query() {
538        let srv = GraphQLServer::new(4332)
539            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
540        let url = srv.start().expect("start should succeed");
541        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
542        let (status, body) = post_graphql(
543            &format!("{url}/graphql"),
544            &serde_json::json!({"query": "{ listUsers { id name } }"}).to_string(),
545        )
546        .await;
547        assert_eq!(status, 200);
548        let users = body["data"]["listUsers"]
549            .as_array()
550            .expect("listUsers must be an array");
551        assert_eq!(users.len(), 2);
552        assert_eq!(users[0]["id"], "1");
553        assert_eq!(users[1]["id"], "2");
554    }
555
556    #[cfg(feature = "real")]
557    #[tokio::test]
558    #[ignore = "requires the real GraphQL server (feature `real`)"]
559    async fn test_real_http_post_unknown_query_returns_errors() {
560        let srv = GraphQLServer::new(4333)
561            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
562        let url = srv.start().expect("start should succeed");
563        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
564        let (status, body) = post_graphql(
565            &format!("{url}/graphql"),
566            &serde_json::json!({"query": "{ unknownQuery { id } }"}).to_string(),
567        )
568        .await;
569        assert_eq!(status, 200);
570        let errors = body["errors"].as_array().expect("errors must be an array");
571        assert!(!errors.is_empty());
572        assert!(errors[0]["message"]
573            .as_str()
574            .expect("message must be a string")
575            .contains("unknownQuery"));
576    }
577
578    #[cfg(feature = "real")]
579    #[test]
580    #[ignore = "requires the real GraphQL engine (feature `real`)"]
581    fn test_real_execute_query_matches_mock_shape() {
582        let srv = GraphQLServer::new(4334)
583            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
584        let single = srv
585            .execute_query("{ getUser(id: 1) { id name } }")
586            .expect("single query should succeed");
587        assert_eq!(single["id"], "1");
588        assert!(single["name"]
589            .as_str()
590            .expect("name must be a string")
591            .contains("getUser"));
592        let list = srv
593            .execute_query("{ listUsers { id name } }")
594            .expect("list query should succeed");
595        assert_eq!(list.as_array().expect("result must be an array").len(), 2);
596        let err = srv
597            .execute_query("{ unknownQuery { id } }")
598            .expect_err("unknown query must fail");
599        assert!(err.contains("unknownQuery"));
600    }
601}