Skip to main content

fraiseql_cli/
cli.rs

1//! CLI argument definitions: `Cli` struct, `Commands` enum, and all sub-command enums.
2
3use clap::{Parser, Subcommand};
4
5/// Exit codes documented in help text
6pub(crate) const EXIT_CODES_HELP: &str = "\
7EXIT CODES:
8    0  Success - Command completed successfully
9    1  Error - Command failed with an error
10    2  Validation failed - Schema or input validation failed";
11
12/// FraiseQL CLI - Compile GraphQL schemas to optimized SQL execution
13#[derive(Parser)]
14#[command(name = "fraiseql")]
15#[command(author, version, about, long_about = None)]
16#[command(propagate_version = true)]
17#[command(after_help = EXIT_CODES_HELP)]
18pub(crate) struct Cli {
19    /// Enable verbose logging
20    #[arg(short, long, global = true)]
21    pub(crate) verbose: bool,
22
23    /// Enable debug logging
24    #[arg(short, long, global = true)]
25    pub(crate) debug: bool,
26
27    /// Output as JSON (machine-readable)
28    #[arg(long, global = true)]
29    pub(crate) json: bool,
30
31    /// Suppress output (exit code only)
32    #[arg(short, long, global = true)]
33    pub(crate) quiet: bool,
34
35    #[command(subcommand)]
36    pub(crate) command: Commands,
37}
38
39#[derive(Subcommand)]
40pub(crate) enum Commands {
41    /// Compile schema to optimized schema.compiled.json
42    ///
43    /// Supports three workflows:
44    /// 1. TOML-only: fraiseql compile fraiseql.toml
45    /// 2. Language + TOML: fraiseql compile fraiseql.toml --types types.json
46    /// 3. Legacy JSON: fraiseql compile schema.json
47    #[command(after_help = "\
48EXAMPLES:
49    fraiseql compile fraiseql.toml
50    fraiseql compile fraiseql.toml --types types.json
51    fraiseql compile schema.json -o schema.compiled.json
52    fraiseql compile fraiseql.toml --check")]
53    Compile {
54        /// Input file path: fraiseql.toml (TOML) or schema.json (legacy)
55        #[arg(value_name = "INPUT")]
56        input: String,
57
58        /// Optional types.json from language implementation (used with fraiseql.toml)
59        #[arg(long, value_name = "TYPES")]
60        types: Option<String>,
61
62        /// Directory for auto-discovery of schema files (recursive *.json)
63        #[arg(long, value_name = "DIR")]
64        schema_dir: Option<String>,
65
66        /// Type files (repeatable): fraiseql compile fraiseql.toml --type-file a.json --type-file
67        /// b.json
68        #[arg(long = "type-file", value_name = "FILE")]
69        type_files: Vec<String>,
70
71        /// Query files (repeatable)
72        #[arg(long = "query-file", value_name = "FILE")]
73        query_files: Vec<String>,
74
75        /// Mutation files (repeatable)
76        #[arg(long = "mutation-file", value_name = "FILE")]
77        mutation_files: Vec<String>,
78
79        /// Output schema.compiled.json file path
80        #[arg(
81            short,
82            long,
83            value_name = "OUTPUT",
84            default_value = "schema.compiled.json"
85        )]
86        output: String,
87
88        /// Validate only, don't write output
89        #[arg(long)]
90        check: bool,
91
92        /// Optional database URL for indexed column validation
93        /// When provided, validates that indexed columns exist in database views
94        #[arg(long, value_name = "DATABASE_URL")]
95        database: Option<String>,
96    },
97
98    /// Extract schema from annotated source files
99    ///
100    /// Parses FraiseQL annotations in any supported language and generates schema.json.
101    /// No language runtime required — pure text processing.
102    #[command(after_help = "\
103EXAMPLES:
104    fraiseql extract schema/schema.py
105    fraiseql extract schema/ --recursive
106    fraiseql extract schema.rs --language rust -o schema.json")]
107    Extract {
108        /// Source file(s) or directory to extract from
109        #[arg(value_name = "INPUT")]
110        input: Vec<String>,
111
112        /// Override language detection (python, typescript, rust, java, kotlin, go, csharp, swift,
113        /// scala)
114        #[arg(short, long)]
115        language: Option<String>,
116
117        /// Recursively scan directories
118        #[arg(short, long)]
119        recursive: bool,
120
121        /// Output file path
122        #[arg(short, long, default_value = "schema.json")]
123        output: String,
124    },
125
126    /// Explain query execution plan and complexity
127    ///
128    /// Shows GraphQL query execution plan, SQL, and complexity analysis.
129    #[command(after_help = "\
130EXAMPLES:
131    fraiseql explain '{ users { id name } }'
132    fraiseql explain '{ user(id: 1) { posts { title } } }' --json")]
133    Explain {
134        /// GraphQL query string
135        #[arg(value_name = "QUERY")]
136        query: String,
137    },
138
139    /// Calculate query complexity score
140    ///
141    /// Quick analysis of query complexity (depth, field count, score).
142    #[command(after_help = "\
143EXAMPLES:
144    fraiseql cost '{ users { id name } }'
145    fraiseql cost '{ deeply { nested { query { here } } } }' --json")]
146    Cost {
147        /// GraphQL query string
148        #[arg(value_name = "QUERY")]
149        query: String,
150    },
151
152    /// Analyze schema for optimization opportunities
153    ///
154    /// Provides recommendations across 6 categories:
155    /// performance, security, federation, complexity, caching, indexing
156    #[command(after_help = "\
157EXAMPLES:
158    fraiseql analyze schema.compiled.json
159    fraiseql analyze schema.compiled.json --json")]
160    Analyze {
161        /// Path to schema.compiled.json
162        #[arg(value_name = "SCHEMA")]
163        schema: String,
164    },
165
166    /// Analyze schema type dependencies
167    ///
168    /// Exports dependency graph, detects cycles, and finds unused types.
169    /// Supports multiple output formats for visualization and CI integration.
170    #[command(after_help = "\
171EXAMPLES:
172    fraiseql dependency-graph schema.compiled.json
173    fraiseql dependency-graph schema.compiled.json -f dot > graph.dot
174    fraiseql dependency-graph schema.compiled.json -f mermaid
175    fraiseql dependency-graph schema.compiled.json --json")]
176    DependencyGraph {
177        /// Path to schema.compiled.json
178        #[arg(value_name = "SCHEMA")]
179        schema: String,
180
181        /// Output format (json, dot, mermaid, d2, console)
182        #[arg(short, long, value_name = "FORMAT", default_value = "json")]
183        format: String,
184    },
185
186    /// Export federation dependency graph
187    ///
188    /// Visualize federation structure in multiple formats.
189    #[command(after_help = "\
190EXAMPLES:
191    fraiseql federation graph schema.compiled.json
192    fraiseql federation graph schema.compiled.json -f dot
193    fraiseql federation graph schema.compiled.json -f mermaid")]
194    Federation {
195        /// Schema path (positional argument passed to subcommand)
196        #[command(subcommand)]
197        command: FederationCommands,
198    },
199
200    /// Lint schema for FraiseQL design quality
201    ///
202    /// Analyzes schema using FraiseQL-calibrated design rules.
203    /// Detects JSONB batching issues, compilation problems, auth boundaries, etc.
204    #[command(after_help = "\
205EXAMPLES:
206    fraiseql lint schema.json
207    fraiseql lint schema.compiled.json --federation
208    fraiseql lint schema.json --fail-on-critical
209    fraiseql lint schema.json --json")]
210    Lint {
211        /// Path to schema.json or schema.compiled.json
212        #[arg(value_name = "SCHEMA")]
213        schema: String,
214
215        /// Only show federation audit
216        #[arg(long)]
217        federation: bool,
218
219        /// Only show cost audit
220        #[arg(long)]
221        cost: bool,
222
223        /// Only show cache audit
224        #[arg(long)]
225        cache: bool,
226
227        /// Only show auth audit
228        #[arg(long)]
229        auth: bool,
230
231        /// Only show compilation audit
232        #[arg(long)]
233        compilation: bool,
234
235        /// Exit with error if any critical issues found
236        #[arg(long)]
237        fail_on_critical: bool,
238
239        /// Exit with error if any warning or critical issues found
240        #[arg(long)]
241        fail_on_warning: bool,
242
243        /// Show detailed issue descriptions
244        #[arg(long)]
245        verbose: bool,
246    },
247
248    /// Generate DDL for Arrow views (va_*, tv_*, ta_*)
249    #[command(after_help = "\
250EXAMPLES:
251    fraiseql generate-views -s schema.json -e User --view va_users
252    fraiseql generate-views -s schema.json -e Order --view tv_orders --refresh-strategy scheduled")]
253    GenerateViews {
254        /// Path to schema.json
255        #[arg(short, long, value_name = "SCHEMA")]
256        schema: String,
257
258        /// Entity name from schema
259        #[arg(short, long, value_name = "NAME")]
260        entity: String,
261
262        /// View name (must start with va_, tv_, or ta_)
263        #[arg(long, value_name = "NAME")]
264        view: String,
265
266        /// Refresh strategy (trigger-based or scheduled)
267        #[arg(long, value_name = "STRATEGY", default_value = "trigger-based")]
268        refresh_strategy: String,
269
270        /// Output file path (default: {view}.sql)
271        #[arg(short, long, value_name = "PATH")]
272        output: Option<String>,
273
274        /// Include helper/composition views
275        #[arg(long, default_value = "true")]
276        include_composition_views: bool,
277
278        /// Include monitoring functions
279        #[arg(long, default_value = "true")]
280        include_monitoring: bool,
281
282        /// Validate only, don't write file
283        #[arg(long)]
284        validate: bool,
285
286        /// Show generation steps (use global --verbose flag)
287        #[arg(long, action = clap::ArgAction::SetTrue)]
288        gen_verbose: bool,
289    },
290
291    /// Validate schema.json or fact tables
292    ///
293    /// Performs comprehensive schema validation including:
294    /// - JSON structure validation
295    /// - Type reference validation
296    /// - Circular dependency detection (with --check-cycles)
297    /// - Unused type detection (with --check-unused)
298    #[command(after_help = "\
299EXAMPLES:
300    fraiseql validate schema.json
301    fraiseql validate schema.json --check-unused
302    fraiseql validate schema.json --strict
303    fraiseql validate facts -s schema.json -d postgres://localhost/db")]
304    Validate {
305        #[command(subcommand)]
306        command: Option<ValidateCommands>,
307
308        /// Schema.json file path to validate (if no subcommand)
309        #[arg(value_name = "INPUT")]
310        input: Option<String>,
311
312        /// Check for circular dependencies between types
313        #[arg(long, default_value = "true")]
314        check_cycles: bool,
315
316        /// Check for unused types (no incoming references)
317        #[arg(long)]
318        check_unused: bool,
319
320        /// Strict mode: treat warnings as errors (unused types become errors)
321        #[arg(long)]
322        strict: bool,
323
324        /// Only analyze specific type(s) - comma-separated list
325        #[arg(long, value_name = "TYPES", value_delimiter = ',')]
326        types: Vec<String>,
327    },
328
329    /// Introspect database for fact tables and output suggestions
330    #[command(after_help = "\
331EXAMPLES:
332    fraiseql introspect facts -d postgres://localhost/db
333    fraiseql introspect facts -d postgres://localhost/db -f json")]
334    Introspect {
335        #[command(subcommand)]
336        command: IntrospectCommands,
337    },
338
339    /// Generate authoring-language source from schema.json
340    ///
341    /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
342    /// source code in any of the 9 supported authoring languages.
343    #[command(after_help = "\
344EXAMPLES:
345    fraiseql generate schema.json --language python
346    fraiseql generate schema.json --language rust -o schema.rs
347    fraiseql generate schema.json --language typescript")]
348    Generate {
349        /// Path to schema.json
350        #[arg(value_name = "INPUT")]
351        input: String,
352
353        /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
354        #[arg(short, long)]
355        language: String,
356
357        /// Output file path (default: schema.<ext> based on language)
358        #[arg(short, long)]
359        output: Option<String>,
360    },
361
362    /// Initialize a new FraiseQL project
363    ///
364    /// Creates project directory with fraiseql.toml, schema.json,
365    /// database DDL structure, and authoring skeleton.
366    #[command(after_help = "\
367EXAMPLES:
368    fraiseql init my-app
369    fraiseql init my-app --language typescript --database postgres
370    fraiseql init my-app --size xs --no-git")]
371    Init {
372        /// Project name (used as directory name)
373        #[arg(value_name = "PROJECT_NAME")]
374        project_name: String,
375
376        /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
377        #[arg(short, long, default_value = "python")]
378        language: String,
379
380        /// Target database (postgres, mysql, sqlite, sqlserver)
381        #[arg(long, default_value = "postgres")]
382        database: String,
383
384        /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
385        #[arg(long, default_value = "s")]
386        size: String,
387
388        /// Skip git init
389        #[arg(long)]
390        no_git: bool,
391    },
392
393    /// Run database migrations
394    ///
395    /// Wraps confiture for a unified migration experience.
396    /// Reads database URL from --database, fraiseql.toml, or DATABASE_URL env var.
397    #[command(after_help = "\
398EXAMPLES:
399    fraiseql migrate up --database postgres://localhost/mydb
400    fraiseql migrate down --steps 1
401    fraiseql migrate status
402    fraiseql migrate create add_posts_table")]
403    Migrate {
404        #[command(subcommand)]
405        command: MigrateCommands,
406    },
407
408    /// Generate Software Bill of Materials
409    ///
410    /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
411    #[command(after_help = "\
412EXAMPLES:
413    fraiseql sbom
414    fraiseql sbom --format spdx
415    fraiseql sbom --format cyclonedx --output sbom.json")]
416    Sbom {
417        /// Output format (cyclonedx, spdx)
418        #[arg(short, long, default_value = "cyclonedx")]
419        format: String,
420
421        /// Output file path (default: stdout)
422        #[arg(short, long, value_name = "FILE")]
423        output: Option<String>,
424    },
425
426    /// Compile schema and immediately start the GraphQL server
427    ///
428    /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
429    /// With --watch, the server hot-reloads whenever the schema file changes.
430    ///
431    /// Server and database settings can be declared in fraiseql.toml under [server]
432    /// and [database] sections.  CLI flags take precedence over TOML settings, which
433    /// take precedence over defaults.  The database URL is resolved in this order:
434    /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
435    #[cfg(feature = "run-server")]
436    #[command(after_help = "\
437EXAMPLES:
438    fraiseql run
439    fraiseql run fraiseql.toml --database postgres://localhost/mydb
440    fraiseql run --port 3000 --watch
441    fraiseql run schema.json --introspection
442
443TOML CONFIG:
444    [server]
445    host = \"127.0.0.1\"
446    port = 9000
447
448    [server.cors]
449    origins = [\"https://app.example.com\"]
450
451    [database]
452    url      = \"${DATABASE_URL}\"
453    pool_min = 2
454    pool_max = 20")]
455    Run {
456        /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
457        #[arg(value_name = "INPUT")]
458        input: Option<String>,
459
460        /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
461        #[arg(short, long, value_name = "DATABASE_URL")]
462        database: Option<String>,
463
464        /// Port to listen on (overrides [server].port in fraiseql.toml)
465        #[arg(short, long, value_name = "PORT")]
466        port: Option<u16>,
467
468        /// Bind address (overrides [server].host in fraiseql.toml)
469        #[arg(long, value_name = "HOST")]
470        bind: Option<String>,
471
472        /// Watch input file for changes and hot-reload the server
473        #[arg(short, long)]
474        watch: bool,
475
476        /// Enable the GraphQL introspection endpoint (no auth required)
477        #[arg(long)]
478        introspection: bool,
479    },
480
481    /// Validate a trusted documents manifest
482    ///
483    /// Checks that the manifest JSON is well-formed and that each key
484    /// is a valid SHA-256 hex string matching its query body.
485    #[command(after_help = "\
486EXAMPLES:
487    fraiseql validate-documents manifest.json")]
488    ValidateDocuments {
489        /// Path to the trusted documents manifest JSON file
490        #[arg(value_name = "MANIFEST")]
491        manifest: String,
492    },
493
494    /// Development server with hot-reload
495    #[command(hide = true)] // Hide until implemented
496    Serve {
497        /// Schema.json file path to watch
498        #[arg(value_name = "SCHEMA")]
499        schema: String,
500
501        /// Port to listen on
502        #[arg(short, long, default_value = "8080")]
503        port: u16,
504    },
505
506    /// Run diagnostic checks for common FraiseQL setup problems
507    ///
508    /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
509    /// and cache/auth coherence. Prints a color-coded report and exits 0 if
510    /// all checks pass or 1 if any check fails (warnings do not fail).
511    #[command(after_help = "\
512EXAMPLES:
513    fraiseql doctor
514    fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
515    fraiseql doctor --db-url postgres://user:pass@host:5432/db
516    fraiseql doctor --json")]
517    Doctor {
518        /// Path to fraiseql.toml configuration file.
519        #[arg(long, default_value = "fraiseql.toml")]
520        config: std::path::PathBuf,
521
522        /// Path to schema.compiled.json.
523        #[arg(long, default_value = "schema.compiled.json")]
524        schema: std::path::PathBuf,
525
526        /// Override DATABASE_URL for the connectivity check.
527        #[arg(long)]
528        db_url: Option<String>,
529
530        /// Output machine-readable JSON (for CI integration).
531        #[arg(long)]
532        json: bool,
533    },
534}
535
536#[derive(Subcommand)]
537pub(crate) enum ValidateCommands {
538    /// Validate that declared fact tables match database schema
539    Facts {
540        /// Schema.json file path
541        #[arg(short, long, value_name = "SCHEMA")]
542        schema: String,
543
544        /// Database connection string
545        #[arg(short, long, value_name = "DATABASE_URL")]
546        database: String,
547    },
548}
549
550#[derive(Subcommand)]
551pub(crate) enum FederationCommands {
552    /// Export federation graph
553    Graph {
554        /// Path to schema.compiled.json
555        #[arg(value_name = "SCHEMA")]
556        schema: String,
557
558        /// Output format (json, dot, mermaid)
559        #[arg(short, long, value_name = "FORMAT", default_value = "json")]
560        format: String,
561    },
562}
563
564#[derive(Subcommand)]
565pub(crate) enum IntrospectCommands {
566    /// Introspect database for fact tables (tf_* tables)
567    Facts {
568        /// Database connection string
569        #[arg(short, long, value_name = "DATABASE_URL")]
570        database: String,
571
572        /// Output format (python, json)
573        #[arg(short, long, value_name = "FORMAT", default_value = "python")]
574        format: String,
575    },
576}
577
578#[derive(Subcommand)]
579pub(crate) enum MigrateCommands {
580    /// Apply pending migrations
581    Up {
582        /// Database connection URL
583        #[arg(long, value_name = "DATABASE_URL")]
584        database: Option<String>,
585
586        /// Migration directory
587        #[arg(long, value_name = "DIR")]
588        dir: Option<String>,
589    },
590
591    /// Roll back migrations
592    Down {
593        /// Database connection URL
594        #[arg(long, value_name = "DATABASE_URL")]
595        database: Option<String>,
596
597        /// Migration directory
598        #[arg(long, value_name = "DIR")]
599        dir: Option<String>,
600
601        /// Number of migrations to roll back
602        #[arg(long, default_value = "1")]
603        steps: u32,
604    },
605
606    /// Show migration status
607    Status {
608        /// Database connection URL
609        #[arg(long, value_name = "DATABASE_URL")]
610        database: Option<String>,
611
612        /// Migration directory
613        #[arg(long, value_name = "DIR")]
614        dir: Option<String>,
615    },
616
617    /// Create a new migration file
618    Create {
619        /// Migration name
620        #[arg(value_name = "NAME")]
621        name: String,
622
623        /// Migration directory
624        #[arg(long, value_name = "DIR")]
625        dir: Option<String>,
626    },
627}