Skip to main content

fraiseql_cli/
runner.rs

1//! CLI command dispatch and helper utilities.
2
3use std::{env, process, str::FromStr};
4
5use clap::{CommandFactory, Parser};
6use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
7
8use crate::cli::{
9    Cli, Commands, FederationCommands, IntrospectCommands, MigrateCommands, ValidateCommands,
10};
11
12/// Run the FraiseQL CLI. Called from both the `fraiseql-cli` and `fraiseql` binary entry points.
13#[allow(clippy::cognitive_complexity)] // Reason: CLI dispatch with many subcommand branches
14pub async fn run() {
15    use crate::{commands, output};
16
17    if let Some(code) = handle_introspection_flags() {
18        process::exit(code);
19    }
20
21    let cli = Cli::parse();
22
23    init_logging(cli.verbose, cli.debug);
24
25    let result = match cli.command {
26        Commands::Compile {
27            input,
28            types,
29            schema_dir,
30            type_files,
31            query_files,
32            mutation_files,
33            output,
34            check,
35            database,
36        } => {
37            commands::compile::run(
38                &input,
39                types.as_deref(),
40                schema_dir.as_deref(),
41                type_files,
42                query_files,
43                mutation_files,
44                &output,
45                check,
46                database.as_deref(),
47            )
48            .await
49        },
50
51        Commands::Extract {
52            input,
53            language,
54            recursive,
55            output,
56        } => commands::extract::run(&input, language.as_deref(), recursive, &output),
57
58        Commands::Explain { query } => match commands::explain::run(&query) {
59            Ok(result) => {
60                println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
61                Ok(())
62            },
63            Err(e) => Err(e),
64        },
65
66        Commands::Cost { query } => match commands::cost::run(&query) {
67            Ok(result) => {
68                println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
69                Ok(())
70            },
71            Err(e) => Err(e),
72        },
73
74        Commands::Analyze { schema } => match commands::analyze::run(&schema) {
75            Ok(result) => {
76                println!("{}", output::OutputFormatter::new(cli.json, cli.quiet).format(&result));
77                Ok(())
78            },
79            Err(e) => Err(e),
80        },
81
82        Commands::DependencyGraph { schema, format } => {
83            match commands::dependency_graph::GraphFormat::from_str(&format) {
84                Ok(fmt) => match commands::dependency_graph::run(&schema, fmt) {
85                    Ok(result) => {
86                        println!(
87                            "{}",
88                            output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
89                        );
90                        Ok(())
91                    },
92                    Err(e) => Err(e),
93                },
94                Err(e) => Err(anyhow::anyhow!(e)),
95            }
96        },
97
98        Commands::Lint {
99            schema,
100            federation,
101            cost,
102            cache,
103            auth,
104            compilation,
105            fail_on_critical,
106            fail_on_warning,
107            verbose: _,
108        } => {
109            let opts = commands::lint::LintOptions {
110                fail_on_critical,
111                fail_on_warning,
112                filter: commands::lint::LintCategoryFilter {
113                    federation,
114                    cost,
115                    cache,
116                    auth,
117                    compilation,
118                },
119            };
120            match commands::lint::run(&schema, opts) {
121                Ok(result) => {
122                    println!(
123                        "{}",
124                        output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
125                    );
126                    Ok(())
127                },
128                Err(e) => Err(e),
129            }
130        },
131
132        Commands::Federation { command } => match command {
133            FederationCommands::Graph { schema, format } => {
134                match commands::federation::graph::GraphFormat::from_str(&format) {
135                    Ok(fmt) => match commands::federation::graph::run(&schema, fmt) {
136                        Ok(result) => {
137                            println!(
138                                "{}",
139                                output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
140                            );
141                            Ok(())
142                        },
143                        Err(e) => Err(e),
144                    },
145                    Err(e) => Err(anyhow::anyhow!(e)),
146                }
147            },
148        },
149
150        Commands::GenerateViews {
151            schema,
152            entity,
153            view,
154            refresh_strategy,
155            output,
156            include_composition_views,
157            include_monitoring,
158            validate,
159            gen_verbose,
160        } => match commands::generate_views::RefreshStrategy::parse(&refresh_strategy) {
161            Ok(refresh_strat) => {
162                let config = commands::generate_views::GenerateViewsConfig {
163                    schema_path: schema,
164                    entity,
165                    view,
166                    refresh_strategy: refresh_strat,
167                    output,
168                    include_composition_views,
169                    include_monitoring,
170                    validate_only: validate,
171                    verbose: cli.verbose || gen_verbose,
172                };
173
174                let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
175                commands::generate_views::run(config, &formatter)
176            },
177            Err(e) => Err(anyhow::anyhow!(e)),
178        },
179
180        Commands::Validate {
181            command,
182            input,
183            check_cycles,
184            check_unused,
185            strict,
186            types,
187        } => match command {
188            Some(ValidateCommands::Facts { schema, database }) => {
189                let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
190                commands::validate_facts::run(std::path::Path::new(&schema), &database, &formatter)
191                    .await
192            },
193            None => match input {
194                Some(input) => {
195                    let opts = commands::validate::ValidateOptions {
196                        check_cycles,
197                        check_unused,
198                        strict,
199                        filter_types: types,
200                    };
201                    match commands::validate::run_with_options(&input, opts) {
202                        Ok(result) => {
203                            println!(
204                                "{}",
205                                output::OutputFormatter::new(cli.json, cli.quiet).format(&result)
206                            );
207                            if result.status == "validation-failed" {
208                                Err(anyhow::anyhow!("Validation failed"))
209                            } else {
210                                Ok(())
211                            }
212                        },
213                        Err(e) => Err(e),
214                    }
215                },
216                None => Err(anyhow::anyhow!("INPUT required when no subcommand provided")),
217            },
218        },
219
220        Commands::Introspect { command } => match command {
221            IntrospectCommands::Facts { database, format } => {
222                match commands::introspect_facts::OutputFormat::parse(&format) {
223                    Ok(fmt) => {
224                        let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
225                        commands::introspect_facts::run(&database, fmt, &formatter).await
226                    },
227                    Err(e) => Err(anyhow::anyhow!(e)),
228                }
229            },
230        },
231
232        Commands::Generate {
233            input,
234            language,
235            output,
236        } => match commands::init::Language::from_str(&language) {
237            Ok(lang) => commands::generate::run(&input, lang, output.as_deref()),
238            Err(e) => Err(anyhow::anyhow!(e)),
239        },
240
241        Commands::Init {
242            project_name,
243            language,
244            database,
245            size,
246            no_git,
247        } => {
248            match (
249                commands::init::Language::from_str(&language),
250                commands::init::Database::from_str(&database),
251                commands::init::ProjectSize::from_str(&size),
252            ) {
253                (Ok(lang), Ok(db), Ok(sz)) => {
254                    let config = commands::init::InitConfig {
255                        project_name,
256                        language: lang,
257                        database: db,
258                        size: sz,
259                        no_git,
260                    };
261                    commands::init::run(&config)
262                },
263                (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(anyhow::anyhow!(e)),
264            }
265        },
266
267        Commands::Migrate { command } => {
268            let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
269            match command {
270                MigrateCommands::Up { database, dir } => {
271                    let db_url = commands::migrate::resolve_database_url(database.as_deref());
272                    match db_url {
273                        Ok(url) => {
274                            let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
275                            let action = commands::migrate::MigrateAction::Up {
276                                database_url: url,
277                                dir:          mig_dir,
278                            };
279                            commands::migrate::run(&action, &formatter)
280                        },
281                        Err(e) => Err(e),
282                    }
283                },
284                MigrateCommands::Down {
285                    database,
286                    dir,
287                    steps,
288                } => {
289                    let db_url = commands::migrate::resolve_database_url(database.as_deref());
290                    match db_url {
291                        Ok(url) => {
292                            let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
293                            let action = commands::migrate::MigrateAction::Down {
294                                database_url: url,
295                                dir: mig_dir,
296                                steps,
297                            };
298                            commands::migrate::run(&action, &formatter)
299                        },
300                        Err(e) => Err(e),
301                    }
302                },
303                MigrateCommands::Status { database, dir } => {
304                    let db_url = commands::migrate::resolve_database_url(database.as_deref());
305                    match db_url {
306                        Ok(url) => {
307                            let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
308                            let action = commands::migrate::MigrateAction::Status {
309                                database_url: url,
310                                dir:          mig_dir,
311                            };
312                            commands::migrate::run(&action, &formatter)
313                        },
314                        Err(e) => Err(e),
315                    }
316                },
317                MigrateCommands::Create { name, dir } => {
318                    let mig_dir = commands::migrate::resolve_migration_dir(dir.as_deref());
319                    let action = commands::migrate::MigrateAction::Create { name, dir: mig_dir };
320                    commands::migrate::run(&action, &formatter)
321                },
322            }
323        },
324
325        Commands::Sbom { format, output } => match commands::sbom::SbomFormat::from_str(&format) {
326            Ok(fmt) => commands::sbom::run(fmt, output.as_deref()),
327            Err(e) => Err(anyhow::anyhow!(e)),
328        },
329
330        #[cfg(feature = "run-server")]
331        Commands::Run {
332            input,
333            database,
334            port,
335            bind,
336            watch,
337            introspection,
338        } => commands::run::run(input.as_deref(), database, port, bind, watch, introspection).await,
339
340        Commands::ValidateDocuments { manifest } => {
341            let formatter = output::OutputFormatter::new(cli.json, cli.quiet);
342            match commands::validate_documents::run(&manifest, &formatter) {
343                Ok(true) => Ok(()),
344                Ok(false) => {
345                    process::exit(2);
346                },
347                Err(e) => Err(e),
348            }
349        },
350
351        Commands::Serve { schema, port } => commands::serve::run(&schema, port).await,
352
353        Commands::Doctor {
354            config,
355            schema,
356            db_url,
357            json: json_flag,
358        } => {
359            let all_passed = commands::doctor::run(&config, &schema, db_url.as_deref(), json_flag);
360            if all_passed {
361                Ok(())
362            } else {
363                process::exit(1);
364            }
365        },
366    };
367
368    if let Err(e) = result {
369        eprintln!("Error: {e}");
370        if cli.debug {
371            eprintln!("\nDebug info:");
372            eprintln!("{e:?}");
373        }
374        process::exit(1);
375    }
376}
377
378fn init_logging(verbose: bool, debug: bool) {
379    let filter = if debug {
380        "fraiseql=debug,fraiseql_core=debug"
381    } else if verbose {
382        "fraiseql=info,fraiseql_core=info"
383    } else {
384        "fraiseql=warn,fraiseql_core=warn"
385    };
386
387    tracing_subscriber::registry()
388        .with(
389            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| filter.into()),
390        )
391        .with(tracing_subscriber::fmt::layer())
392        .init();
393}
394
395/// Serialize a value to a JSON `Value`, printing to stderr and exiting with code 2 on failure.
396fn serialize_or_exit<T: serde::Serialize>(value: &T, context: &str) -> serde_json::Value {
397    serde_json::to_value(value).unwrap_or_else(|e| {
398        eprintln!("fraiseql: failed to serialize {context}: {e}");
399        std::process::exit(2);
400    })
401}
402
403/// Serialize a value to pretty-printed JSON, printing to stderr and exiting with code 2 on failure.
404fn pretty_or_exit<T: serde::Serialize>(value: &T, context: &str) -> String {
405    serde_json::to_string_pretty(value).unwrap_or_else(|e| {
406        eprintln!("fraiseql: failed to format {context}: {e}");
407        std::process::exit(2);
408    })
409}
410
411fn handle_introspection_flags() -> Option<i32> {
412    let args: Vec<String> = env::args().collect();
413
414    if args.iter().any(|a| a == "--help-json") {
415        let cmd = Cli::command();
416        let version = env!("CARGO_PKG_VERSION");
417        let help = crate::introspection::extract_cli_help(&cmd, version);
418        let result =
419            crate::output::CommandResult::success("help", serialize_or_exit(&help, "help output"));
420        println!("{}", pretty_or_exit(&result, "command result"));
421        return Some(0);
422    }
423
424    if args.iter().any(|a| a == "--list-commands") {
425        let cmd = Cli::command();
426        let commands = crate::introspection::list_commands(&cmd);
427        let result = crate::output::CommandResult::success(
428            "list-commands",
429            serialize_or_exit(&commands, "command list"),
430        );
431        println!("{}", pretty_or_exit(&result, "command result"));
432        return Some(0);
433    }
434
435    let idx = args.iter().position(|a| a == "--show-output-schema")?;
436    let available = crate::output_schemas::list_schema_commands().join(", ");
437
438    let Some(cmd_name) = args.get(idx + 1) else {
439        let result = crate::output::CommandResult::error(
440            "show-output-schema",
441            &format!("Missing command name. Available: {available}"),
442            "MISSING_ARGUMENT",
443        );
444        println!("{}", pretty_or_exit(&result, "command result"));
445        return Some(1);
446    };
447
448    if let Some(schema) = crate::output_schemas::get_output_schema(cmd_name) {
449        let result = crate::output::CommandResult::success(
450            "show-output-schema",
451            serialize_or_exit(&schema, "output schema"),
452        );
453        println!("{}", pretty_or_exit(&result, "command result"));
454        return Some(0);
455    }
456
457    let result = crate::output::CommandResult::error(
458        "show-output-schema",
459        &format!("Unknown command: {cmd_name}. Available: {available}"),
460        "UNKNOWN_COMMAND",
461    );
462    println!("{}", pretty_or_exit(&result, "command result"));
463    Some(1)
464}