sz-orm-graphql 6.2.0

GraphQL schema generator and server: SDL generation, model-driven Query/Mutation, optional async-graphql runtime with injectable DB resolver (P2-1: real DB resolver support; falls back to mock data when no resolver is injected)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Real GraphQL execution and serving backed by async-graphql + axum.
//!
//! This module is only compiled with the `real` feature enabled. It builds a
//! dynamic async-graphql schema from the declarative [`GraphQLSchema`],
//! executes queries against it and exposes it over HTTP via axum.
//!
//! # P1-1 / P2-1 说明:root field resolver
//!
//! ## P2-1 修复 C-3:DB resolver 注入(当前状态)
//!
//! 自 P2-1 起,本模块支持通过 [`crate::resolver::DbResolver`] 注入真实数据源。
//! 调用方通过 `GraphQLServer::with_db_resolver` 注入 resolver 后,root field
//! 将通过 resolver 异步查询真实数据库(见 [`resolver_root_field`])。
//! 未注入 resolver 时,回退到 [`mock_root_field`] 返回硬编码 mock 数据(向后兼容)。
//!
//! 这意味着:
//! - ✅ **Schema 构建是真实的**:使用 `async-graphql::dynamic::Schema`,类型/字段定义
//!   均根据 `GraphQLSchema` 动态注册
//! - ✅ **GraphQL 执行是真实的**:查询由 `async-graphql` 引擎按 GraphQL 规范解析与执行
//! - ✅ **HTTP 服务是真实的**:通过 axum 暴露 `/graphql` POST 端点
//! - ✅ **数据解析可注入真实 DB resolver**(P2-1):注入 resolver 后查询真实数据库
//! - ⚠️ **未注入 resolver 时数据为 mock**:root field 返回硬编码数据
//!
//! ## 如何接入真实数据源
//!
//! 实现 [`crate::resolver::DbResolver`] trait,并通过
//! `GraphQLServer::with_db_resolver` 注入即可,无需修改本模块代码。

use async_graphql::dynamic::{
    Field, FieldFuture, InputValue, Object, ResolverContext, Schema, TypeRef,
};
use async_graphql::Value;

use crate::{GraphQLField, GraphQLSchema, GraphQLType};

/// Parse a GraphQL type reference like `ID!`, `User` or `[User!]!` into a
/// dynamic [`TypeRef`]. List nesting beyond one level is rejected.
fn parse_type_ref(type_name: &str) -> Result<TypeRef, String> {
    let unsupported = || format!("Unsupported type reference '{type_name}'");
    let trimmed = type_name.trim();
    let (inner, non_null) = match trimmed.strip_suffix('!') {
        Some(rest) => (rest.trim(), true),
        None => (trimmed, false),
    };
    if let Some(list_inner) = inner.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
        let list_inner = list_inner.trim();
        let (item, item_non_null) = match list_inner.strip_suffix('!') {
            Some(rest) => (rest.trim(), true),
            None => (list_inner, false),
        };
        if item.is_empty() || item.starts_with('[') {
            return Err(unsupported());
        }
        Ok(match (item_non_null, non_null) {
            (false, false) => TypeRef::named_list(item),
            (true, false) => TypeRef::named_nn_list(item),
            (false, true) => TypeRef::named_list_nn(item),
            (true, true) => TypeRef::named_nn_list_nn(item),
        })
    } else if inner.is_empty() {
        Err(unsupported())
    } else if non_null {
        Ok(TypeRef::named_nn(inner))
    } else {
        Ok(TypeRef::named(inner))
    }
}

/// Build the mock JSON payload for a root field, mirroring the data shape of
/// the in-memory implementation.
fn mock_payload(field_name: &str, is_list: bool) -> Value {
    let object = |id: &str| {
        serde_json::json!({
            "id": id,
            "name": format!("{field_name}_{id}"),
            "createdAt": "2024-01-01T00:00:00Z",
            "updatedAt": "2024-01-01T00:00:00Z",
        })
    };
    let json = if is_list {
        serde_json::json!([object("1"), object("2")])
    } else {
        object("1")
    };
    // The payload contains only strings, arrays and objects, which always
    // convert into a GraphQL value.
    Value::from_json(json).unwrap_or(Value::Null)
}

/// Create a root (query/mutation) field that resolves to mock data.
fn mock_root_field(field: &GraphQLField) -> Result<Field, String> {
    let type_ref = parse_type_ref(&field.type_name)?;
    let is_list = field.type_name.trim_start().starts_with('[');
    let value = mock_payload(&field.name, is_list);
    let mut root = Field::new(field.name.clone(), type_ref, move |_ctx| {
        FieldFuture::from_value(Some(value.clone()))
    });
    if !is_list {
        // Single-object lookups conventionally accept an optional `id`.
        root = root.argument(InputValue::new("id", TypeRef::named(TypeRef::ID)));
    }
    Ok(root)
}

/// Create a root field backed by a real DB resolver — P2-1 修复 C-3
///
/// 当调用方注入了 DbResolver 时,root field 通过 resolver 异步查询真实数据库,
/// 而非返回 mock 数据。闭包从 async-graphql 执行上下文提取参数,构造
/// [`crate::resolver::ResolverContext`] 后调用 `DbResolver::resolve_query`。
fn resolver_root_field(
    field: &GraphQLField,
    resolver: crate::resolver::SharedDbResolver,
) -> Result<Field, String> {
    let type_ref = parse_type_ref(&field.type_name)?;
    let is_list = field.type_name.trim_start().starts_with('[');
    let field_name = field.name.clone();
    let type_name = field.type_name.clone();
    let resolver = resolver;

    let mut root = Field::new(
        field.name.clone(),
        type_ref,
        move |ctx: ResolverContext<'_>| {
            // 从 GraphQL 执行上下文提取参数
            let mut args = serde_json::Map::new();
            for (key, val) in ctx.args.iter() {
                if let Ok(json_v) = val.as_value().clone().into_json() {
                    args.insert(key.to_string(), json_v);
                }
            }
            let resolver_ctx = crate::resolver::ResolverContext {
                field_name: field_name.clone(),
                type_name: type_name.clone(),
                is_list,
                args: serde_json::Value::Object(args),
            };
            let resolver = resolver.clone();
            FieldFuture::new(async move {
                match resolver.resolve_query(&resolver_ctx).await {
                    Ok(value) => {
                        let gql_value = Value::from_json(value).unwrap_or(Value::Null);
                        Ok(Some(gql_value))
                    }
                    Err(msg) => {
                        tracing::error!(
                            field = %resolver_ctx.field_name,
                            error = %msg,
                            "DB resolver failed"
                        );
                        Ok(Some(Value::Null))
                    }
                }
            })
        },
    );

    if !is_list {
        root = root.argument(InputValue::new("id", TypeRef::named(TypeRef::ID)));
    }
    Ok(root)
}

/// Create a dynamic object type whose fields read from the parent value
/// resolved by the root field.
fn object_type(t: &GraphQLType) -> Result<Object, String> {
    let mut obj = Object::new(t.name.clone());
    for field in &t.fields {
        let type_ref = parse_type_ref(&field.type_name)?;
        let field_name = field.name.clone();
        obj = obj.field(Field::new(
            field.name.clone(),
            type_ref,
            move |ctx: ResolverContext<'_>| {
                let value = ctx
                    .parent_value
                    .try_to_value()
                    .ok()
                    .and_then(|parent| match parent {
                        Value::Object(map) => map.get(field_name.as_str()).cloned(),
                        _ => None,
                    });
                FieldFuture::from_value(value)
            },
        ));
    }
    Ok(obj)
}

/// Build a real executable async-graphql [`Schema`] from the declarative
/// [`GraphQLSchema`].
///
/// 当 `resolver` 为 `Some` 时,root field 通过 [`resolver_root_field`] 查询真实
/// 数据库;为 `None` 时回退到 [`mock_root_field`](向后兼容)。
pub fn build_dynamic_schema(
    schema: &GraphQLSchema,
    resolver: Option<&crate::resolver::SharedDbResolver>,
) -> Result<Schema, String> {
    let mutation_name = if schema.mutations.is_empty() {
        None
    } else {
        Some("Mutation")
    };
    let mut builder = Schema::build("Query", mutation_name, None);
    for t in &schema.types {
        builder = builder.register(object_type(t)?);
    }
    let mut query = Object::new("Query");
    for field in &schema.queries {
        let f = match resolver {
            Some(r) => resolver_root_field(field, std::sync::Arc::clone(r))?,
            None => mock_root_field(field)?,
        };
        query = query.field(f);
    }
    builder = builder.register(query);
    if mutation_name.is_some() {
        let mut mutation = Object::new("Mutation");
        for field in &schema.mutations {
            let f = match resolver {
                Some(r) => resolver_root_field(field, std::sync::Arc::clone(r))?,
                None => mock_root_field(field)?,
            };
            mutation = mutation.field(f);
        }
        builder = builder.register(mutation);
    }
    builder.finish().map_err(|e| e.to_string())
}

/// Build an axum router serving GraphQL POST requests at `/graphql`.
pub fn router(schema: Schema) -> axum::Router {
    async fn graphql_handler(
        axum::extract::State(schema): axum::extract::State<Schema>,
        request: async_graphql_axum::GraphQLRequest,
    ) -> async_graphql_axum::GraphQLResponse {
        let inner = request.into_inner();
        // v4.8.0 修复 M-3:真实服务端接入查询复杂度/深度限制(需
        // `graphql-complexity` feature)。修复前 complexity.rs 独立存在但
        // 从未接入——深层嵌套/多别名查询可耗尽解析与执行 CPU。超限查询
        // 在执行前拒绝。
        #[cfg(feature = "graphql-complexity")]
        {
            if let Ok(ir) = crate::query_ir::parse_query(&inner.query, None) {
                let calculator = crate::complexity::ComplexityCalculator::with_defaults();
                let result = calculator.calculate(&ir);
                if let Some(err) = result.exceeded {
                    let resp = async_graphql::Response::from_errors(vec![
                        async_graphql::ServerError::new(format!("query rejected: {err}"), None),
                    ]);
                    return resp.into();
                }
            }
            // 解析失败交由 async-graphql 自身返回语法错误
        }
        schema.execute(inner).await.into()
    }

    axum::Router::new()
        .route("/graphql", axum::routing::post(graphql_handler))
        .with_state(schema)
}

/// 异步执行 GraphQL 查询,返回第一个根字段的解析结果(JSON)。
///
/// v1.2.0 修复 P0:新增异步入口,直接 `schema.execute(query).await`,
/// 供异步调用方使用,避免在 async 上下文中调用 `block_on` 导致 tokio
/// runtime 死锁。
///
/// # 推荐用法
///
/// - 调用方处于 async 上下文(`async fn` / tokio task)时,**必须**使用本方法
/// - 调用方处于同步上下文时,使用 [`execute`](同步包装)
#[allow(dead_code)]
pub async fn execute_async(schema: &Schema, query: &str) -> Result<serde_json::Value, String> {
    let response = schema.execute(query).await;
    response_to_json(response)
}

/// 将 async-graphql 响应转换为「第一个根字段的解析结果」JSON。
fn response_to_json(response: async_graphql::Response) -> Result<serde_json::Value, String> {
    if !response.errors.is_empty() {
        return Err(response
            .errors
            .iter()
            .map(|e| e.message.as_str())
            .collect::<Vec<_>>()
            .join("; "));
    }
    let data = response.data.into_json().map_err(|e| e.to_string())?;
    match data {
        serde_json::Value::Object(map) => Ok(map
            .into_iter()
            .next()
            .map(|(_, value)| value)
            .unwrap_or(serde_json::Value::Null)),
        other => Ok(other),
    }
}

/// 同步执行 GraphQL 查询,返回第一个根字段的解析结果(JSON)。
///
/// # 死锁安全性说明
///
/// 本函数为同步 API。它通过 `std::thread::scope` 在**独立的 OS 线程**上
/// 创建**全新的 current_thread tokio runtime** 再 `block_on`,因此:
///
/// - **不会**复用调用方已有的 tokio runtime,不会在已有 runtime 的 worker
///   线程上调用 `block_on`,故**不会死锁**
/// - 可安全地从 async 上下文中调用(但会阻塞调用线程直到查询完成)
///
/// # 调用方建议
///
/// - **同步上下文**:直接使用本函数
/// - **异步上下文**:优先使用 [`execute_async`],避免阻塞当前 async 线程;
///   若必须使用同步 API,本函数也不会死锁,但会占用一个阻塞线程位
pub fn execute(schema: &Schema, query: &str) -> Result<serde_json::Value, String> {
    // 在独立 OS 线程上创建全新 runtime 再 block_on,确保不复用调用方的
    // tokio runtime,从根本上避免在 async 上下文调用 block_on 的死锁风险。
    let response = std::thread::scope(|scope| {
        scope
            .spawn(|| -> Result<async_graphql::Response, String> {
                let runtime = tokio::runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                    .map_err(|e| format!("Failed to build tokio runtime: {e}"))?;
                Ok(runtime.block_on(schema.execute(query)))
            })
            .join()
            .map_err(|_| "GraphQL executor thread panicked".to_string())?
    })?;
    response_to_json(response)
}

#[cfg(test)]
mod tests {
    use crate::resolver::{DbResolver, ResolverContext, SharedDbResolver};
    use crate::{GraphQLSchemaGenerator, GraphQLServer};
    use serde_json::json;
    use std::future::Future;
    use std::pin::Pin;
    use std::sync::Arc;

    /// 测试用 DB resolver — 返回包含 "real" 标记的数据
    struct TestDbResolver;

    impl DbResolver for TestDbResolver {
        fn resolve_query(
            &self,
            ctx: &ResolverContext,
        ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>> {
            let field_name = ctx.field_name.clone();
            let is_list = ctx.is_list;
            Box::pin(async move {
                if is_list {
                    Ok(json!([
                        {"id": "100", "name": format!("{}_real_100", field_name), "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"},
                        {"id": "200", "name": format!("{}_real_200", field_name), "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z"}
                    ]))
                } else {
                    Ok(json!({
                        "id": "100",
                        "name": format!("{}_real", field_name),
                        "createdAt": "2024-01-01T00:00:00Z",
                        "updatedAt": "2024-01-01T00:00:00Z"
                    }))
                }
            })
        }
    }

    #[test]
    fn test_resolver_returns_real_data_single() {
        let resolver: SharedDbResolver = Arc::new(TestDbResolver);
        let srv = GraphQLServer::new(4501)
            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]))
            .with_db_resolver(resolver);
        let result = srv.execute_query("{ getUser(id: 1) { id name } }");
        assert!(result.is_ok(), "expected ok, got {:?}", result);
        let v = result.unwrap();
        assert_eq!(v["id"], "100");
        assert!(
            v["name"].as_str().unwrap().contains("real"),
            "name should contain 'real': {}",
            v["name"]
        );
    }

    #[test]
    fn test_resolver_returns_real_data_list() {
        let resolver: SharedDbResolver = Arc::new(TestDbResolver);
        let srv = GraphQLServer::new(4502)
            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]))
            .with_db_resolver(resolver);
        let result = srv.execute_query("{ listUsers { id name } }");
        assert!(result.is_ok(), "expected ok, got {:?}", result);
        let v = result.unwrap();
        assert!(v.is_array());
        let arr = v.as_array().unwrap();
        assert_eq!(arr.len(), 2);
        assert_eq!(arr[0]["id"], "100");
        assert_eq!(arr[1]["id"], "200");
        assert!(arr[0]["name"].as_str().unwrap().contains("real"));
    }

    #[test]
    fn test_no_resolver_falls_back_to_mock() {
        // 不注入 resolver 时,应回退到 mock 数据
        let srv = GraphQLServer::new(4503)
            .with_schema(GraphQLSchemaGenerator::generate_schema(&["users"]));
        let result = srv.execute_query("{ getUser(id: 1) { id name } }");
        assert!(result.is_ok(), "expected ok, got {:?}", result);
        let v = result.unwrap();
        assert_eq!(v["id"], "1"); // mock 数据 id 为 "1"
        assert!(
            v["name"].as_str().unwrap().contains("getUser"),
            "mock name should contain field name"
        );
    }
}