cli/commands/internal/graphql/
inspect.rs

1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, clap::Args)]
5pub struct Inspect {
6    /// The GraphQL endpoint to inspect (e.g., 'account', 'sync_status', 'send_payment')
7    pub endpoint: String,
8
9    /// GraphQL server URL
10    #[arg(long, default_value = "http://localhost:3000/graphql")]
11    pub node: String,
12}
13
14#[derive(Debug, Serialize)]
15struct GraphQLRequest {
16    query: String,
17}
18
19#[derive(Debug, Deserialize)]
20struct GraphQLResponse {
21    data: Option<serde_json::Value>,
22    errors: Option<Vec<GraphQLError>>,
23}
24
25#[derive(Debug, Deserialize)]
26struct GraphQLError {
27    message: String,
28}
29
30impl Inspect {
31    pub fn run(self) -> Result<()> {
32        // GraphQL introspection query to get field details
33        let introspection_query = r#"
34            query IntrospectSchema {
35                __schema {
36                    queryType { name }
37                    mutationType { name }
38                    types {
39                        name
40                        kind
41                        description
42                        fields {
43                            name
44                            description
45                            args {
46                                name
47                                description
48                                type {
49                                    name
50                                    kind
51                                    ofType {
52                                        name
53                                        kind
54                                        ofType {
55                                            name
56                                            kind
57                                        }
58                                    }
59                                }
60                                defaultValue
61                            }
62                            type {
63                                name
64                                kind
65                                ofType {
66                                    name
67                                    kind
68                                    ofType {
69                                        name
70                                        kind
71                                    }
72                                }
73                            }
74                        }
75                        inputFields {
76                            name
77                            description
78                            type {
79                                name
80                                kind
81                                ofType {
82                                    name
83                                    kind
84                                    ofType {
85                                        name
86                                        kind
87                                    }
88                                }
89                            }
90                        }
91                    }
92                }
93            }
94        "#;
95
96        let request = GraphQLRequest {
97            query: introspection_query.to_string(),
98        };
99
100        // Make the introspection request
101        let client = reqwest::blocking::Client::new();
102        let response = client
103            .post(&self.node)
104            .json(&request)
105            .send()
106            .map_err(|e| anyhow!("Failed to connect to GraphQL server: {}", e))?;
107
108        if !response.status().is_success() {
109            return Err(anyhow!(
110                "GraphQL server returned error: {}",
111                response.status()
112            ));
113        }
114
115        let graphql_response: GraphQLResponse = response
116            .json()
117            .map_err(|e| anyhow!("Failed to parse GraphQL response: {}", e))?;
118
119        if let Some(errors) = graphql_response.errors {
120            for error in errors {
121                eprintln!("GraphQL Error: {}", error.message);
122            }
123            return Err(anyhow!("GraphQL introspection failed"));
124        }
125
126        let data = graphql_response
127            .data
128            .ok_or_else(|| anyhow!("No data in GraphQL response"))?;
129
130        // Parse the schema and find the requested endpoint
131        let (_has_required_args, args) = self.display_endpoint_info(&data)?;
132
133        // Show example with or without variables
134        self.display_example_output(&args)?;
135
136        Ok(())
137    }
138
139    fn display_endpoint_info(
140        &self,
141        schema_data: &serde_json::Value,
142    ) -> Result<(bool, Vec<(String, bool)>)> {
143        let schema = schema_data
144            .get("__schema")
145            .ok_or_else(|| anyhow!("Invalid schema response"))?;
146
147        let types = schema
148            .get("types")
149            .and_then(|v| v.as_array())
150            .ok_or_else(|| anyhow!("Invalid types in schema"))?;
151
152        // Find Query or Mutation type
153        let query_type_name = schema
154            .get("queryType")
155            .and_then(|v| v.get("name"))
156            .and_then(|v| v.as_str());
157
158        let mutation_type_name = schema
159            .get("mutationType")
160            .and_then(|v| v.get("name"))
161            .and_then(|v| v.as_str());
162
163        let mut found = false;
164        let mut has_required_args = false;
165        let mut args_info = Vec::new();
166
167        // Search in Query type
168        if let Some(query_name) = query_type_name {
169            if let Some(query_type) = types.iter().find(|t| {
170                t.get("name")
171                    .and_then(|n| n.as_str())
172                    .map(|n| n == query_name)
173                    .unwrap_or(false)
174            }) {
175                if let Some(fields) = query_type.get("fields").and_then(|f| f.as_array()) {
176                    if let Some(field) = fields.iter().find(|f| {
177                        f.get("name")
178                            .and_then(|n| n.as_str())
179                            .map(|n| n == self.endpoint)
180                            .unwrap_or(false)
181                    }) {
182                        println!("Endpoint: {} (Query)", self.endpoint);
183                        let (has_req, args) = self.display_field(field)?;
184                        has_required_args = has_req;
185                        args_info = args;
186                        found = true;
187                    }
188                }
189            }
190        }
191
192        // Search in Mutation type if not found in Query
193        if !found {
194            if let Some(mutation_name) = mutation_type_name {
195                if let Some(mutation_type) = types.iter().find(|t| {
196                    t.get("name")
197                        .and_then(|n| n.as_str())
198                        .map(|n| n == mutation_name)
199                        .unwrap_or(false)
200                }) {
201                    if let Some(fields) = mutation_type.get("fields").and_then(|f| f.as_array()) {
202                        if let Some(field) = fields.iter().find(|f| {
203                            f.get("name")
204                                .and_then(|n| n.as_str())
205                                .map(|n| n == self.endpoint)
206                                .unwrap_or(false)
207                        }) {
208                            println!("Endpoint: {} (Mutation)", self.endpoint);
209                            let (has_req, args) = self.display_field(field)?;
210                            has_required_args = has_req;
211                            args_info = args;
212                            found = true;
213                        }
214                    }
215                }
216            }
217        }
218
219        if !found {
220            return Err(anyhow!(
221                "Endpoint '{}' not found. Use 'mina internal graphql list' to see available endpoints.",
222                self.endpoint
223            ));
224        }
225
226        Ok((has_required_args, args_info))
227    }
228
229    fn display_field(&self, field: &serde_json::Value) -> Result<(bool, Vec<(String, bool)>)> {
230        println!();
231
232        // Description
233        if let Some(desc) = field.get("description").and_then(|d| d.as_str()) {
234            println!("Description:");
235            println!("  {}", desc);
236            println!();
237        }
238
239        // Arguments
240        let mut has_required_args = false;
241        let mut args_info = Vec::new();
242        if let Some(args) = field.get("args").and_then(|a| a.as_array()) {
243            if !args.is_empty() {
244                println!("Arguments:");
245                for arg in args {
246                    let name = arg.get("name").and_then(|n| n.as_str()).unwrap_or("");
247                    let is_required = self.is_required_type(arg.get("type"));
248                    let desc = arg
249                        .get("description")
250                        .and_then(|d| d.as_str())
251                        .unwrap_or("");
252
253                    if is_required {
254                        has_required_args = true;
255                    }
256
257                    args_info.push((name.to_string(), is_required));
258
259                    let requirement = if is_required {
260                        " (required)"
261                    } else {
262                        " (optional)"
263                    };
264                    print!("  {}{}", name, requirement);
265                    if !desc.is_empty() {
266                        print!(" - {}", desc);
267                    }
268                    println!();
269                }
270                println!();
271            }
272        }
273
274        Ok((has_required_args, args_info))
275    }
276
277    fn is_required_type(&self, type_val: Option<&serde_json::Value>) -> bool {
278        if let Some(type_val) = type_val {
279            if let Some(kind) = type_val.get("kind").and_then(|k| k.as_str()) {
280                return kind == "NON_NULL";
281            }
282        }
283        false
284    }
285
286    fn display_example_output(&self, args: &[(String, bool)]) -> Result<()> {
287        if args.is_empty() {
288            // Simple query without arguments
289            println!("\nExample Query:");
290            println!("  query {{");
291            println!("    {}", self.endpoint);
292            println!("  }}");
293            println!();
294
295            // Show curl command
296            let query_escaped = format!("query {{ {} }}", self.endpoint).replace('"', "\\\"");
297            println!("Curl Command:");
298            println!("  curl -X POST {} \\", self.node);
299            println!("    -H \"Content-Type: application/json\" \\");
300            println!("    -d '{{\"query\": \"{}\"}}'", query_escaped);
301            println!();
302
303            // Show CLI run command
304            println!("CLI Command:");
305            println!(
306                "  mina internal graphql run 'query {{ {} }}' \\",
307                self.endpoint
308            );
309            println!("    --node {}", self.node);
310            println!();
311
312            // Execute the example query
313            let query = format!("query {{ {} }}", self.endpoint);
314            self.execute_and_display_query(&query)?;
315        } else {
316            // Query with arguments - show example with variables
317            println!("\nExample Query with Variables:");
318            let arg_list: Vec<String> = args
319                .iter()
320                .map(|(name, is_req)| {
321                    let type_suffix = if *is_req { "!" } else { "" };
322                    format!("${}: Type{}", name, type_suffix)
323                })
324                .collect();
325            let arg_usage: Vec<String> = args
326                .iter()
327                .map(|(name, _)| format!("{}: ${}", name, name))
328                .collect();
329
330            println!("  query({}) {{", arg_list.join(", "));
331            println!("    {}({})", self.endpoint, arg_usage.join(", "));
332            println!("  }}");
333            println!();
334
335            // Generate example variable values
336            let example_vars: Vec<String> = args
337                .iter()
338                .map(|(name, _)| {
339                    // Provide sensible default values based on common arg names
340                    let value = match name.as_str() {
341                        "maxLength" | "max" | "limit" => "10",
342                        "offset" | "skip" => "0",
343                        _ => "\"example_value\"",
344                    };
345                    format!("\"{}\": {}", name, value)
346                })
347                .collect();
348
349            println!("Example Variables:");
350            println!("  {{");
351            for (i, var) in example_vars.iter().enumerate() {
352                if i < example_vars.len() - 1 {
353                    println!("    {},", var);
354                } else {
355                    println!("    {}", var);
356                }
357            }
358            println!("  }}");
359            println!();
360
361            // Show CLI run command with variables
362            println!("CLI Command:");
363            let query_with_vars = format!(
364                "query({}) {{ {}({}) }}",
365                arg_list.join(", "),
366                self.endpoint,
367                arg_usage.join(", ")
368            );
369            println!("  mina internal graphql run \\");
370            println!("    '{}' \\", query_with_vars);
371            println!("    -v '{{{}}}' \\", example_vars.join(", "));
372            println!("    --node {}", self.node);
373            println!();
374
375            println!(
376                "Note: Adjust the variable values and types according to the endpoint's schema."
377            );
378            println!("      Use 'mina internal graphql run --help' for more information.");
379            println!();
380        }
381
382        Ok(())
383    }
384
385    fn execute_and_display_query(&self, query: &str) -> Result<()> {
386        let request = GraphQLRequest {
387            query: query.to_string(),
388        };
389
390        let client = reqwest::blocking::Client::new();
391        let response = client
392            .post(&self.node)
393            .json(&request)
394            .send()
395            .map_err(|e| anyhow!("Failed to execute example query: {}", e))?;
396
397        if !response.status().is_success() {
398            println!(
399                "Warning: Could not fetch example output (status: {})",
400                response.status()
401            );
402            return Ok(());
403        }
404
405        let graphql_response: GraphQLResponse = response
406            .json()
407            .map_err(|e| anyhow!("Failed to parse example response: {}", e))?;
408
409        println!("Example Response:");
410        if let Some(data) = graphql_response.data {
411            let formatted = serde_json::to_string_pretty(&data)
412                .unwrap_or_else(|_| serde_json::to_string(&data).unwrap_or_default());
413            println!("{}", formatted);
414        }
415
416        if let Some(errors) = graphql_response.errors {
417            if !errors.is_empty() {
418                println!("\nErrors:");
419                for error in errors {
420                    println!("  - {}", error.message);
421                }
422            }
423        }
424
425        Ok(())
426    }
427}