1mod execute;
7mod extract;
8
9pub use execute::{
10 build_graphql_document, build_selection_set, execute_graphql, validate_required_graphql_args,
11};
12pub use extract::{
13 extract_graphql_commands, graphql_type_string, graphql_type_to_python, unwrap_type,
14};
15
16use serde_json::{json, Value};
17
18use crate::cache::{cache_key_for, load_cached, save_cache};
19use crate::error::{Error, Result};
20use crate::paths::DEFAULT_CACHE_TTL;
21
22pub const GRAPHQL_INTROSPECTION_QUERY: &str = r#"
24query IntrospectionQuery {
25 __schema {
26 queryType { name }
27 mutationType { name }
28 types {
29 kind
30 name
31 fields(includeDeprecated: false) {
32 name
33 description
34 args {
35 name
36 description
37 type {
38 ...TypeRef
39 }
40 defaultValue
41 }
42 type {
43 ...TypeRef
44 }
45 }
46 inputFields {
47 name
48 description
49 type {
50 ...TypeRef
51 }
52 defaultValue
53 }
54 enumValues(includeDeprecated: false) {
55 name
56 description
57 }
58 }
59 }
60}
61
62fragment TypeRef on __Type {
63 kind
64 name
65 ofType {
66 kind
67 name
68 ofType {
69 kind
70 name
71 ofType {
72 kind
73 name
74 ofType {
75 kind
76 name
77 }
78 }
79 }
80 }
81}
82"#;
83
84pub fn load_graphql_schema(
86 url: &str,
87 auth_headers: &[(String, String)],
88 cache_key: Option<&str>,
89 ttl: Option<u64>,
90 refresh: bool,
91) -> Result<Value> {
92 let ttl = ttl.unwrap_or(DEFAULT_CACHE_TTL);
93 let key = cache_key.map(str::to_string).unwrap_or_else(|| {
94 cache_key_for(&json!({
95 "source": format!("graphql:{url}"),
96 "auth_headers": auth_headers,
97 }))
98 });
99 if !refresh {
100 if let Some(cached) = load_cached(&key, ttl)? {
101 return Ok(cached);
102 }
103 }
104
105 let client = reqwest::blocking::Client::builder()
106 .timeout(std::time::Duration::from_secs(30))
107 .build()
108 .map_err(|e| Error::runtime(e.to_string()))?;
109 let mut req = client.post(url).header("Content-Type", "application/json");
110 for (k, v) in auth_headers {
111 req = req.header(k, v);
112 }
113 let resp = req
114 .json(&json!({ "query": GRAPHQL_INTROSPECTION_QUERY }))
115 .send()
116 .map_err(|e| Error::runtime(format!("GraphQL introspection request failed: {e}")))?;
117 if !resp.status().is_success() {
118 return Err(Error::runtime(format!(
119 "Error {}: {}",
120 resp.status().as_u16(),
121 resp.text().unwrap_or_default()
122 )));
123 }
124 let result: Value = resp
125 .json()
126 .map_err(|e| Error::runtime(format!("invalid JSON from GraphQL introspection: {e}")))?;
127
128 if result.get("errors").is_some() && result.get("data").is_none() {
129 let msgs = result["errors"]
130 .as_array()
131 .map(|arr| {
132 arr.iter()
133 .filter_map(|e| e.get("message").and_then(|m| m.as_str()))
134 .collect::<Vec<_>>()
135 .join("; ")
136 })
137 .unwrap_or_default();
138 return Err(Error::runtime(format!(
139 "GraphQL introspection failed: {msgs}"
140 )));
141 }
142
143 let schema = result
144 .pointer("/data/__schema")
145 .cloned()
146 .ok_or_else(|| Error::runtime("introspection returned no schema"))?;
147 if !schema.is_object() {
148 return Err(Error::runtime("introspection returned no schema"));
149 }
150
151 save_cache(&key, &schema)?;
152 Ok(schema)
153}