Skip to main content

skiff_cli/graphql/
mod.rs

1//! GraphQL introspection → CLI commands → execute.
2//!
3//! Introspection results are cached like OpenAPI (`$SKIFF_CACHE_DIR`). Use
4//! `--fields` to override the auto-built selection set.
5
6mod 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
22/// Fixed introspection query (parity with Python mcp2cli).
23pub 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
84/// POST introspection query to a GraphQL endpoint, with caching.
85pub 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}