cli/commands/internal/graphql/
inspect.rs1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, clap::Args)]
5pub struct Inspect {
6 pub endpoint: String,
8
9 #[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 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 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 let (_has_required_args, args) = self.display_endpoint_info(&data)?;
132
133 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 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 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 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 if let Some(desc) = field.get("description").and_then(|d| d.as_str()) {
234 println!("Description:");
235 println!(" {}", desc);
236 println!();
237 }
238
239 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 println!("\nExample Query:");
290 println!(" query {{");
291 println!(" {}", self.endpoint);
292 println!(" }}");
293 println!();
294
295 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 println!("CLI Command:");
305 println!(
306 " mina internal graphql run 'query {{ {} }}' \\",
307 self.endpoint
308 );
309 println!(" --node {}", self.node);
310 println!();
311
312 let query = format!("query {{ {} }}", self.endpoint);
314 self.execute_and_display_query(&query)?;
315 } else {
316 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 let example_vars: Vec<String> = args
337 .iter()
338 .map(|(name, _)| {
339 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 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}