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 /// Skip embedding content hash in compiled schema (for test fixtures)
93 #[arg(long)]
94 skip_hash: bool,
95
96 /// Optional database URL for indexed column validation
97 /// When provided, validates that indexed columns exist in database views
98 #[arg(long, value_name = "DATABASE_URL")]
99 database: Option<String>,
100
101 /// Emit DDL files for all schema types to the given directory
102 ///
103 /// Writes `CREATE TABLE` DDL for each compiled type to `<DIR>/<type>.sql`.
104 /// Output is compatible with confiture's `db/schema/` directory format,
105 /// enabling `fraiseql migrate generate` to auto-detect schema drift.
106 #[arg(long, value_name = "DIR")]
107 emit_ddl: Option<String>,
108
109 /// Check compiled schema against the database for migration drift
110 ///
111 /// Emits DDL to a temporary directory, then delegates to
112 /// `confiture migrate validate` for drift detection.
113 /// Exits non-zero when the compiled schema diverges from the database.
114 #[arg(long)]
115 check_migrations: bool,
116 },
117
118 /// Extract schema from annotated source files
119 ///
120 /// Parses FraiseQL annotations in any supported language and generates schema.json.
121 /// No language runtime required — pure text processing.
122 #[command(after_help = "\
123EXAMPLES:
124 fraiseql extract schema/schema.py
125 fraiseql extract schema/ --recursive
126 fraiseql extract schema.rs --language rust -o schema.json")]
127 Extract {
128 /// Source file(s) or directory to extract from
129 #[arg(value_name = "INPUT")]
130 input: Vec<String>,
131
132 /// Override language detection (python, typescript, rust, java, kotlin, go, csharp, swift,
133 /// scala)
134 #[arg(short, long)]
135 language: Option<String>,
136
137 /// Recursively scan directories
138 #[arg(short, long)]
139 recursive: bool,
140
141 /// Output file path
142 #[arg(short, long, default_value = "schema.json")]
143 output: String,
144 },
145
146 /// Explain query execution plan and complexity
147 ///
148 /// Shows GraphQL query execution plan, SQL, and complexity analysis.
149 #[command(after_help = "\
150EXAMPLES:
151 fraiseql explain '{ users { id name } }'
152 fraiseql explain '{ user(id: 1) { posts { title } } }' --json")]
153 Explain {
154 /// GraphQL query string
155 #[arg(value_name = "QUERY")]
156 query: String,
157 },
158
159 /// Calculate query complexity score
160 ///
161 /// Quick analysis of query complexity (depth, field count, score).
162 #[command(after_help = "\
163EXAMPLES:
164 fraiseql cost '{ users { id name } }'
165 fraiseql cost '{ deeply { nested { query { here } } } }' --json")]
166 Cost {
167 /// GraphQL query string
168 #[arg(value_name = "QUERY")]
169 query: String,
170 },
171
172 /// Analyze schema for optimization opportunities
173 ///
174 /// Provides recommendations across 6 categories:
175 /// performance, security, federation, complexity, caching, indexing
176 #[command(after_help = "\
177EXAMPLES:
178 fraiseql analyze schema.compiled.json
179 fraiseql analyze schema.compiled.json --json")]
180 Analyze {
181 /// Path to schema.compiled.json
182 #[arg(value_name = "SCHEMA")]
183 schema: String,
184 },
185
186 /// Analyze schema type dependencies
187 ///
188 /// Exports dependency graph, detects cycles, and finds unused types.
189 /// Supports multiple output formats for visualization and CI integration.
190 #[command(after_help = "\
191EXAMPLES:
192 fraiseql dependency-graph schema.compiled.json
193 fraiseql dependency-graph schema.compiled.json -f dot > graph.dot
194 fraiseql dependency-graph schema.compiled.json -f mermaid
195 fraiseql dependency-graph schema.compiled.json --json")]
196 DependencyGraph {
197 /// Path to schema.compiled.json
198 #[arg(value_name = "SCHEMA")]
199 schema: String,
200
201 /// Output format (json, dot, mermaid, d2, console)
202 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
203 format: String,
204 },
205
206 /// Export federation dependency graph
207 ///
208 /// Visualize federation structure in multiple formats.
209 #[command(after_help = "\
210EXAMPLES:
211 fraiseql federation graph schema.compiled.json
212 fraiseql federation graph schema.compiled.json -f dot
213 fraiseql federation graph schema.compiled.json -f mermaid")]
214 Federation {
215 /// Schema path (positional argument passed to subcommand)
216 #[command(subcommand)]
217 command: FederationCommands,
218 },
219
220 /// Lint schema for FraiseQL design quality
221 ///
222 /// Analyzes schema using FraiseQL-calibrated design rules.
223 /// Detects JSONB batching issues, compilation problems, auth boundaries, etc.
224 #[command(after_help = "\
225EXAMPLES:
226 fraiseql lint schema.json
227 fraiseql lint schema.compiled.json --federation
228 fraiseql lint schema.json --fail-on-critical
229 fraiseql lint schema.json --json")]
230 Lint {
231 /// Path to schema.json or schema.compiled.json
232 #[arg(value_name = "SCHEMA")]
233 schema: String,
234
235 /// Only show federation audit
236 #[arg(long)]
237 federation: bool,
238
239 /// Only show cost audit
240 #[arg(long)]
241 cost: bool,
242
243 /// Only show cache audit
244 #[arg(long)]
245 cache: bool,
246
247 /// Only show auth audit
248 #[arg(long)]
249 auth: bool,
250
251 /// Only show compilation audit
252 #[arg(long)]
253 compilation: bool,
254
255 /// Exit with error if any critical issues found
256 #[arg(long)]
257 fail_on_critical: bool,
258
259 /// Exit with error if any warning or critical issues found
260 #[arg(long)]
261 fail_on_warning: bool,
262
263 /// Show detailed issue descriptions
264 #[arg(long)]
265 verbose: bool,
266 },
267
268 /// Generate DDL for Arrow views (va_*, tv_*, ta_*)
269 #[command(after_help = "\
270EXAMPLES:
271 fraiseql generate-views -s schema.json -e User --view va_users
272 fraiseql generate-views -s schema.json -e Order --view tv_orders --refresh-strategy scheduled")]
273 GenerateViews {
274 /// Path to schema.json
275 #[arg(short, long, value_name = "SCHEMA")]
276 schema: String,
277
278 /// Entity name from schema
279 #[arg(short, long, value_name = "NAME")]
280 entity: String,
281
282 /// View name (must start with va_, tv_, or ta_)
283 #[arg(long, value_name = "NAME")]
284 view: String,
285
286 /// Refresh strategy (trigger-based or scheduled)
287 #[arg(long, value_name = "STRATEGY", default_value = "trigger-based")]
288 refresh_strategy: String,
289
290 /// Output file path (default: {view}.sql)
291 #[arg(short, long, value_name = "PATH")]
292 output: Option<String>,
293
294 /// Include helper/composition views
295 #[arg(long, default_value = "true")]
296 include_composition_views: bool,
297
298 /// Include monitoring functions
299 #[arg(long, default_value = "true")]
300 include_monitoring: bool,
301
302 /// Validate only, don't write file
303 #[arg(long)]
304 validate: bool,
305
306 /// Show generation steps (use global --verbose flag)
307 #[arg(long, action = clap::ArgAction::SetTrue)]
308 gen_verbose: bool,
309 },
310
311 /// Validate schema.json or fact tables
312 ///
313 /// Performs comprehensive schema validation including:
314 /// - JSON structure validation
315 /// - Type reference validation
316 /// - Circular dependency detection (with --check-cycles)
317 /// - Unused type detection (with --check-unused)
318 #[command(after_help = "\
319EXAMPLES:
320 fraiseql validate schema.json
321 fraiseql validate schema.json --check-unused
322 fraiseql validate schema.json --strict
323 fraiseql validate facts -s schema.json -d postgres://localhost/db")]
324 Validate {
325 #[command(subcommand)]
326 command: Option<ValidateCommands>,
327
328 /// Schema.json file path to validate (if no subcommand)
329 #[arg(value_name = "INPUT")]
330 input: Option<String>,
331
332 /// Check for circular dependencies between types
333 #[arg(long, default_value = "true")]
334 check_cycles: bool,
335
336 /// Check for unused types (no incoming references)
337 #[arg(long)]
338 check_unused: bool,
339
340 /// Strict mode: treat warnings as errors (unused types become errors)
341 #[arg(long)]
342 strict: bool,
343
344 /// Only analyze specific type(s) - comma-separated list
345 #[arg(long, value_name = "TYPES", value_delimiter = ',')]
346 types: Vec<String>,
347 },
348
349 /// Introspect database for fact tables and output suggestions
350 #[command(after_help = "\
351EXAMPLES:
352 fraiseql introspect facts -d postgres://localhost/db
353 fraiseql introspect facts -d postgres://localhost/db -f json")]
354 Introspect {
355 #[command(subcommand)]
356 command: IntrospectCommands,
357 },
358
359 /// Generate authoring-language source from schema.json
360 ///
361 /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
362 /// source code in any of the 9 supported authoring languages.
363 #[command(after_help = "\
364EXAMPLES:
365 fraiseql generate schema.json --language python
366 fraiseql generate schema.json --language rust -o schema.rs
367 fraiseql generate schema.json --language typescript")]
368 Generate {
369 /// Path to schema.json
370 #[arg(value_name = "INPUT")]
371 input: String,
372
373 /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
374 #[arg(short, long)]
375 language: String,
376
377 /// Output file path (default: schema.<ext> based on language)
378 #[arg(short, long)]
379 output: Option<String>,
380 },
381
382 /// Initialize a new FraiseQL project
383 ///
384 /// Creates project directory with fraiseql.toml, schema.json,
385 /// database DDL structure, and authoring skeleton.
386 #[command(after_help = "\
387EXAMPLES:
388 fraiseql init my-app
389 fraiseql init my-app --language typescript --database postgres
390 fraiseql init my-app --size xs --no-git")]
391 Init {
392 /// Project name (used as directory name)
393 #[arg(value_name = "PROJECT_NAME")]
394 project_name: String,
395
396 /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
397 #[arg(short, long, default_value = "python")]
398 language: String,
399
400 /// Target database (postgres, mysql, sqlite, sqlserver)
401 #[arg(long, default_value = "postgres")]
402 database: String,
403
404 /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
405 #[arg(long, default_value = "s")]
406 size: String,
407
408 /// Skip git init
409 #[arg(long)]
410 no_git: bool,
411 },
412
413 /// Run database migrations
414 ///
415 /// Wraps confiture for a unified migration experience.
416 /// Reads database URL from --database, fraiseql.toml, or DATABASE_URL env var.
417 #[command(after_help = "\
418EXAMPLES:
419 fraiseql migrate up --database postgres://localhost/mydb
420 fraiseql migrate down --steps 1
421 fraiseql migrate status
422 fraiseql migrate create add_posts_table")]
423 Migrate {
424 #[command(subcommand)]
425 command: MigrateCommands,
426 },
427
428 /// Generate Software Bill of Materials
429 ///
430 /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
431 #[command(after_help = "\
432EXAMPLES:
433 fraiseql sbom
434 fraiseql sbom --format spdx
435 fraiseql sbom --format cyclonedx --output sbom.json")]
436 Sbom {
437 /// Output format (cyclonedx, spdx)
438 #[arg(short, long, default_value = "cyclonedx")]
439 format: String,
440
441 /// Output file path (default: stdout)
442 #[arg(short, long, value_name = "FILE")]
443 output: Option<String>,
444 },
445
446 /// Compile schema and immediately start the GraphQL server
447 ///
448 /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
449 /// With --watch, the server hot-reloads whenever the schema file changes.
450 ///
451 /// Server and database settings can be declared in fraiseql.toml under [server]
452 /// and [database] sections. CLI flags take precedence over TOML settings, which
453 /// take precedence over defaults. The database URL is resolved in this order:
454 /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
455 #[cfg(feature = "run-server")]
456 #[command(after_help = "\
457EXAMPLES:
458 fraiseql run
459 fraiseql run fraiseql.toml --database postgres://localhost/mydb
460 fraiseql run --port 3000 --watch
461 fraiseql run schema.json --introspection
462
463TOML CONFIG:
464 [server]
465 host = \"127.0.0.1\"
466 port = 9000
467
468 [server.cors]
469 origins = [\"https://app.example.com\"]
470
471 [database]
472 url = \"${DATABASE_URL}\"
473 pool_min = 2
474 pool_max = 20")]
475 Run {
476 /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
477 #[arg(value_name = "INPUT")]
478 input: Option<String>,
479
480 /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
481 #[arg(short, long, value_name = "DATABASE_URL")]
482 database: Option<String>,
483
484 /// Port to listen on (overrides [server].port in fraiseql.toml)
485 #[arg(short, long, value_name = "PORT")]
486 port: Option<u16>,
487
488 /// Bind address (overrides [server].host in fraiseql.toml)
489 #[arg(long, value_name = "HOST")]
490 bind: Option<String>,
491
492 /// Watch input file for changes and hot-reload the server
493 #[arg(short, long)]
494 watch: bool,
495
496 /// Enable the GraphQL introspection endpoint (no auth required)
497 #[arg(long)]
498 introspection: bool,
499 },
500
501 /// Validate a trusted documents manifest
502 ///
503 /// Checks that the manifest JSON is well-formed and that each key
504 /// is a valid SHA-256 hex string matching its query body.
505 #[command(after_help = "\
506EXAMPLES:
507 fraiseql validate-documents manifest.json")]
508 ValidateDocuments {
509 /// Path to the trusted documents manifest JSON file
510 #[arg(value_name = "MANIFEST")]
511 manifest: String,
512 },
513
514 /// Development server with hot-reload
515 #[command(hide = true)] // Hide until implemented
516 Serve {
517 /// Schema.json file path to watch
518 #[arg(value_name = "SCHEMA")]
519 schema: String,
520
521 /// Port to listen on
522 #[arg(short, long, default_value = "8080")]
523 port: u16,
524 },
525
526 /// Install FraiseQL mutation helper functions
527 ///
528 /// Installs SQL helper functions (fraiseql.mutation_ok, fraiseql.mutation_err, etc.)
529 /// to reduce boilerplate when writing mutation functions under the v2.2.0 protocol.
530 /// The helpers are installed in the `fraiseql` schema, which is owned by FraiseQL's
531 /// database role.
532 #[command(after_help = "\
533EXAMPLES:
534 fraiseql setup --database postgres://localhost/mydb
535 fraiseql setup --dry-run
536 fraiseql setup # Uses DATABASE_URL or [database].url from fraiseql.toml")]
537 Setup {
538 /// Database connection URL (or use DATABASE_URL env var, or [database].url in
539 /// fraiseql.toml)
540 #[arg(long, value_name = "DATABASE_URL")]
541 database: Option<String>,
542
543 /// Print SQL without applying changes
544 #[arg(long)]
545 dry_run: bool,
546 },
547
548 /// Inspect schema metadata from a running FraiseQL server
549 ///
550 /// Fetches field-level security metadata (encryption, scope requirements, deny actions)
551 /// from the server's `/api/v1/schema/metadata` endpoint and displays it as a table.
552 #[command(after_help = "\
553EXAMPLES:
554 fraiseql schema metadata
555 fraiseql schema metadata --server http://localhost:8080
556 fraiseql schema metadata --server https://api.example.com --token mytoken")]
557 Schema {
558 #[command(subcommand)]
559 command: SchemaCommands,
560 },
561
562 /// Run diagnostic checks for common FraiseQL setup problems
563 ///
564 /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
565 /// and cache/auth coherence. Prints a color-coded report and exits 0 if
566 /// all checks pass or 1 if any check fails (warnings do not fail).
567 #[command(after_help = "\
568EXAMPLES:
569 fraiseql doctor
570 fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
571 fraiseql doctor --db-url postgres://user:pass@host:5432/db
572 fraiseql doctor --json")]
573 Doctor {
574 /// Path to fraiseql.toml configuration file.
575 #[arg(long, default_value = "fraiseql.toml")]
576 config: std::path::PathBuf,
577
578 /// Path to schema.compiled.json.
579 #[arg(long, default_value = "schema.compiled.json")]
580 schema: std::path::PathBuf,
581
582 /// Override DATABASE_URL for the connectivity check.
583 #[arg(long)]
584 db_url: Option<String>,
585
586 /// Output machine-readable JSON (for CI integration).
587 #[arg(long)]
588 json: bool,
589 },
590}
591
592#[derive(Subcommand)]
593pub(crate) enum ValidateCommands {
594 /// Validate that declared fact tables match database schema
595 Facts {
596 /// Schema.json file path
597 #[arg(short, long, value_name = "SCHEMA")]
598 schema: String,
599
600 /// Database connection string
601 #[arg(short, long, value_name = "DATABASE_URL")]
602 database: String,
603 },
604}
605
606#[derive(Subcommand)]
607pub(crate) enum FederationCommands {
608 /// Export federation graph
609 Graph {
610 /// Path to schema.compiled.json
611 #[arg(value_name = "SCHEMA")]
612 schema: String,
613
614 /// Output format (json, dot, mermaid)
615 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
616 format: String,
617 },
618
619 /// Validate subgraph composition
620 Check {
621 /// Path to local schema.compiled.json
622 #[arg(value_name = "SCHEMA")]
623 schema: String,
624
625 /// Path to supergraph schema for composition validation
626 #[arg(short, long, value_name = "SUPERGRAPH")]
627 against: Option<String>,
628
629 /// Output result as JSON
630 #[arg(long)]
631 json: bool,
632 },
633}
634
635#[derive(Subcommand)]
636pub(crate) enum IntrospectCommands {
637 /// Introspect database for fact tables (tf_* tables)
638 Facts {
639 /// Database connection string
640 #[arg(short, long, value_name = "DATABASE_URL")]
641 database: String,
642
643 /// Output format (python, json)
644 #[arg(short, long, value_name = "FORMAT", default_value = "python")]
645 format: String,
646 },
647}
648
649#[derive(Subcommand)]
650pub(crate) enum SchemaCommands {
651 /// Display field-level security metadata from a running server
652 Metadata {
653 /// Server base URL
654 #[arg(
655 short,
656 long,
657 value_name = "URL",
658 default_value = "http://localhost:8080"
659 )]
660 server: String,
661
662 /// Bearer token for authentication
663 #[arg(short, long, value_name = "TOKEN")]
664 token: Option<String>,
665 },
666}
667
668#[derive(Subcommand)]
669pub(crate) enum MigrateCommands {
670 /// Apply pending migrations
671 Up {
672 /// Database connection URL
673 #[arg(long, value_name = "DATABASE_URL")]
674 database: Option<String>,
675
676 /// Migration directory
677 #[arg(long, value_name = "DIR")]
678 dir: Option<String>,
679 },
680
681 /// Roll back migrations
682 Down {
683 /// Database connection URL
684 #[arg(long, value_name = "DATABASE_URL")]
685 database: Option<String>,
686
687 /// Migration directory
688 #[arg(long, value_name = "DIR")]
689 dir: Option<String>,
690
691 /// Number of migrations to roll back
692 #[arg(long, default_value = "1")]
693 steps: u32,
694 },
695
696 /// Show migration status
697 Status {
698 /// Database connection URL
699 #[arg(long, value_name = "DATABASE_URL")]
700 database: Option<String>,
701
702 /// Migration directory
703 #[arg(long, value_name = "DIR")]
704 dir: Option<String>,
705 },
706
707 /// Create a new migration file
708 Create {
709 /// Migration name
710 #[arg(value_name = "NAME")]
711 name: String,
712
713 /// Migration directory
714 #[arg(long, value_name = "DIR")]
715 dir: Option<String>,
716 },
717
718 /// Generate a new migration from schema diff
719 ///
720 /// Delegates to `confiture migrate generate`.
721 /// Creates a timestamped migration file.
722 Generate {
723 /// Migration name
724 #[arg(value_name = "NAME")]
725 name: String,
726
727 /// Migration directory
728 #[arg(long, value_name = "DIR")]
729 dir: Option<String>,
730 },
731
732 /// Validate migration files for naming, idempotency, and drift
733 ///
734 /// Delegates to `confiture migrate validate`.
735 /// Checks naming conventions, idempotency, and schema drift.
736 Validate {
737 /// Migration directory
738 #[arg(long, value_name = "DIR")]
739 dir: Option<String>,
740 },
741
742 /// Pre-deploy safety check on pending migrations
743 ///
744 /// Delegates to `confiture migrate preflight`.
745 /// Verifies reversibility, detects non-transactional statements, checks checksums.
746 Preflight {
747 /// Migration directory
748 #[arg(long, value_name = "DIR")]
749 dir: Option<String>,
750 },
751}