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: check the schema against the live database
97 ///
98 /// Validates each `sql_source`, mutation function, JSONB key and query argument
99 /// against the relations it names, and fills `native_columns` from the views'
100 /// columns. Drift classified as an error fails the compile (see `--allow-drift`).
101 #[arg(long, value_name = "DATABASE_URL")]
102 database: Option<String>,
103
104 /// Emit DDL files for all schema types to the given directory
105 ///
106 /// Writes `CREATE TABLE` DDL for each compiled type to `<DIR>/<type>.sql`.
107 /// Output is compatible with confiture's `db/schema/` directory format,
108 /// enabling `fraiseql migrate generate` to auto-detect schema drift.
109 #[arg(long, value_name = "DIR")]
110 emit_ddl: Option<String>,
111
112 /// Check compiled schema against the database for migration drift
113 ///
114 /// Emits DDL to a temporary directory, then delegates to
115 /// `confiture migrate validate` for drift detection.
116 /// Exits non-zero when the compiled schema diverges from the database.
117 #[arg(long)]
118 check_migrations: bool,
119
120 /// Compile despite error-severity schema↔database drift (advisory mode)
121 ///
122 /// With `--database`, drift the linter classifies as an error (a
123 /// `sql_source` that does not exist, a required field's JSONB key
124 /// absent from every sampled row, a mutation with no backing function)
125 /// fails the compile. This flag downgrades those findings to reported
126 /// advisories and writes the artifact anyway.
127 #[arg(long, requires = "database")]
128 allow_drift: bool,
129
130 /// Project config to compile with. Default: the nearest fraiseql.toml beside
131 /// or above the input, up to the repository root — never the working directory
132 #[arg(long, value_name = "PATH")]
133 config: Option<String>,
134 },
135
136 /// Extract schema from annotated source files
137 ///
138 /// Parses FraiseQL annotations in any supported language and generates schema.json.
139 /// No language runtime required — pure text processing.
140 #[command(after_help = "\
141EXAMPLES:
142 fraiseql extract schema/schema.py
143 fraiseql extract schema/ --recursive
144 fraiseql extract schema.rs --language rust -o schema.json")]
145 Extract {
146 /// Source file(s) or directory to extract from
147 #[arg(value_name = "INPUT")]
148 input: Vec<String>,
149
150 /// Override language detection (python, typescript, rust, java, kotlin, go, csharp, swift,
151 /// scala)
152 #[arg(short, long)]
153 language: Option<String>,
154
155 /// Recursively scan directories
156 #[arg(short, long)]
157 recursive: bool,
158
159 /// Output file path
160 #[arg(short, long, default_value = "schema.json")]
161 output: String,
162 },
163
164 /// Explain query execution plan and complexity
165 ///
166 /// Shows GraphQL query execution plan, SQL, and complexity analysis.
167 #[command(after_help = "\
168EXAMPLES:
169 fraiseql explain '{ users { id name } }'
170 fraiseql explain '{ user(id: 1) { posts { title } } }' --json")]
171 Explain {
172 /// GraphQL query string
173 #[arg(value_name = "QUERY")]
174 query: String,
175 },
176
177 /// Calculate query complexity score
178 ///
179 /// Quick analysis of query complexity (depth, field count, score).
180 #[command(after_help = "\
181EXAMPLES:
182 fraiseql cost '{ users { id name } }'
183 fraiseql cost '{ deeply { nested { query { here } } } }' --json")]
184 Cost {
185 /// GraphQL query string
186 #[arg(value_name = "QUERY")]
187 query: String,
188 },
189
190 /// Analyze schema for optimization opportunities
191 ///
192 /// Provides recommendations across 6 categories:
193 /// performance, security, federation, complexity, caching, indexing
194 #[command(after_help = "\
195EXAMPLES:
196 fraiseql analyze schema.compiled.json
197 fraiseql analyze schema.compiled.json --json")]
198 Analyze {
199 /// Path to schema.compiled.json
200 #[arg(value_name = "SCHEMA")]
201 schema: String,
202 },
203
204 /// Analyze schema type dependencies
205 ///
206 /// Exports dependency graph, detects cycles, and finds unused types.
207 /// Supports multiple output formats for visualization and CI integration.
208 #[command(after_help = "\
209EXAMPLES:
210 fraiseql dependency-graph schema.compiled.json
211 fraiseql dependency-graph schema.compiled.json -f dot > graph.dot
212 fraiseql dependency-graph schema.compiled.json -f mermaid
213 fraiseql dependency-graph schema.compiled.json --json")]
214 DependencyGraph {
215 /// Path to schema.compiled.json
216 #[arg(value_name = "SCHEMA")]
217 schema: String,
218
219 /// Output format (json, dot, mermaid, d2, console)
220 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
221 format: String,
222 },
223
224 /// Federation tools: draw the graph subgraphs form, check a subgraph
225 ///
226 /// `graph` takes each subgraph's compiled schema and draws who extends whose entities.
227 #[command(after_help = "\
228EXAMPLES:
229 fraiseql federation graph users.compiled.json orders.compiled.json
230 fraiseql federation graph users.compiled.json orders.compiled.json -f dot
231 fraiseql federation graph users.compiled.json orders.compiled.json -f mermaid
232 fraiseql federation check schema.compiled.json --against other.compiled.json
233 fraiseql federation sdl schema.compiled.json -o subgraph.graphql (--features federation)")]
234 Federation {
235 /// Schema path (positional argument passed to subcommand)
236 #[command(subcommand)]
237 command: FederationCommands,
238 },
239
240 /// Lint schema for FraiseQL design quality
241 ///
242 /// Analyzes schema using FraiseQL-calibrated design rules.
243 /// Detects JSONB batching issues, compilation problems, auth boundaries, etc.
244 #[command(after_help = "\
245EXAMPLES:
246 fraiseql lint schema.json
247 fraiseql lint schema.compiled.json --federation
248 fraiseql lint schema.json --fail-on-critical
249 fraiseql lint schema.json --json")]
250 Lint {
251 /// Path to schema.json or schema.compiled.json
252 #[arg(value_name = "SCHEMA")]
253 schema: String,
254
255 /// Only show federation audit
256 #[arg(long)]
257 federation: bool,
258
259 /// Only show cost audit
260 #[arg(long)]
261 cost: bool,
262
263 /// Only show cache audit
264 #[arg(long)]
265 cache: bool,
266
267 /// Only show auth audit
268 #[arg(long)]
269 auth: bool,
270
271 /// Only show compilation audit
272 #[arg(long)]
273 compilation: bool,
274
275 /// Exit with error if any critical issues found
276 #[arg(long)]
277 fail_on_critical: bool,
278
279 /// Exit with error if any warning or critical issues found
280 #[arg(long)]
281 fail_on_warning: bool,
282 },
283
284 /// Generate DDL for Arrow views (va_*, tv_*, ta_*)
285 #[command(after_help = "\
286EXAMPLES:
287 fraiseql generate-views -s schema.json -e User --view va_users
288 fraiseql generate-views -s schema.json -e Order --view tv_orders --refresh-strategy scheduled")]
289 GenerateViews {
290 /// Path to schema.json
291 #[arg(short, long, value_name = "SCHEMA")]
292 schema: String,
293
294 /// Entity name from schema
295 #[arg(short, long, value_name = "NAME")]
296 entity: String,
297
298 /// View name (must start with va_, tv_, or ta_)
299 #[arg(long, value_name = "NAME")]
300 view: String,
301
302 /// Refresh strategy (trigger-based or scheduled)
303 #[arg(long, value_name = "STRATEGY", default_value = "trigger-based")]
304 refresh_strategy: String,
305
306 /// Output file path (default: {view}.sql)
307 #[arg(short, long, value_name = "PATH")]
308 output: Option<String>,
309
310 /// Include helper/composition views
311 #[arg(long, default_value = "true")]
312 include_composition_views: bool,
313
314 /// Include monitoring functions
315 #[arg(long, default_value = "true")]
316 include_monitoring: bool,
317
318 /// Validate the generated DDL by executing it against PostgreSQL
319 /// (DATABASE_URL) in a rolled-back transaction; don't write a file
320 #[arg(long)]
321 validate: bool,
322
323 /// Show generation steps (use global --verbose flag)
324 #[arg(long, action = clap::ArgAction::SetTrue)]
325 gen_verbose: bool,
326 },
327
328 /// Generate external-write capture-trigger DDL for @subscribable types (#366)
329 ///
330 /// Emits a self-contained, idempotent PostgreSQL script (the capture function
331 /// plus per-table statement-level triggers) that brings raw external writes
332 /// (psql / migrations / third-party tools) to a `@subscribable` type's table(s)
333 /// onto the Change Spine, without double-emitting for FraiseQL-mediated writes.
334 #[command(after_help = "\
335EXAMPLES:
336 fraiseql generate-capture-triggers -s schema.compiled.json | psql \"$DATABASE_URL\"
337 fraiseql generate-capture-triggers -s schema.compiled.json -o capture.sql")]
338 GenerateCaptureTriggers {
339 /// Path to the compiled schema (schema.compiled.json)
340 #[arg(short, long, value_name = "SCHEMA")]
341 schema: String,
342
343 /// Output file path (default: stdout, for piping into psql)
344 #[arg(short, long, value_name = "PATH")]
345 output: Option<String>,
346
347 /// Omit the capture function definition (emit only the per-table triggers)
348 #[arg(long)]
349 no_function: bool,
350 },
351
352 /// Validate a compiled schema (schema.compiled.json) or fact tables
353 ///
354 /// Checks the schema `fraiseql compile` wrote, including:
355 /// - JSON structure validation
356 /// - Type reference validation
357 /// - Circular dependency detection (with --check-cycles)
358 /// - Unused type detection (with --check-unused)
359 ///
360 /// An authoring schema.json is checked by `fraiseql compile schema.json --check`.
361 #[command(after_help = "\
362EXAMPLES:
363 fraiseql validate schema.compiled.json
364 fraiseql validate schema.compiled.json --check-unused
365 fraiseql validate schema.compiled.json --strict
366 fraiseql validate facts -s schema.json --database postgres://localhost/db")]
367 Validate {
368 #[command(subcommand)]
369 command: Option<ValidateCommands>,
370
371 /// Compiled schema (schema.compiled.json) to validate (if no subcommand)
372 #[arg(value_name = "INPUT")]
373 input: Option<String>,
374
375 /// Check for circular dependencies between types
376 #[arg(long, default_value = "true")]
377 check_cycles: bool,
378
379 /// Check for unused types (no incoming references)
380 #[arg(long)]
381 check_unused: bool,
382
383 /// Strict mode: treat warnings as errors (unused types become errors)
384 #[arg(long)]
385 strict: bool,
386
387 /// Only analyze specific type(s) - comma-separated list
388 #[arg(long, value_name = "TYPES", value_delimiter = ',')]
389 types: Vec<String>,
390
391 /// Validate the mutation contract against a live PostgreSQL database.
392 ///
393 /// For each DB-backed mutation, checks that its `sql_source` function
394 /// exists with the argument layout the server sends and returns the
395 /// columns the server decodes into `MutationResponse`. Requires a
396 /// compiled schema (schema.compiled.json) as INPUT. (#397)
397 #[arg(long, value_name = "DATABASE_URL")]
398 against_db: Option<String>,
399 },
400
401 /// Introspect database for fact tables and output suggestions
402 #[command(after_help = "\
403EXAMPLES:
404 fraiseql introspect facts --database postgres://localhost/db
405 fraiseql introspect facts --database postgres://localhost/db -f json")]
406 Introspect {
407 #[command(subcommand)]
408 command: IntrospectCommands,
409 },
410
411 /// Generate authoring-language source from schema.json
412 ///
413 /// The inverse of `fraiseql extract`: reads a schema.json and produces annotated
414 /// source code in any of the 9 supported authoring languages.
415 #[command(after_help = "\
416EXAMPLES:
417 fraiseql generate schema.json --language python
418 fraiseql generate schema.json --language rust -o schema.rs
419 fraiseql generate schema.json --language typescript")]
420 Generate {
421 /// Path to schema.json
422 #[arg(value_name = "INPUT")]
423 input: String,
424
425 /// Target language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
426 #[arg(short, long)]
427 language: String,
428
429 /// Output file path (default: schema.<ext> based on language)
430 #[arg(short, long)]
431 output: Option<String>,
432 },
433
434 /// Generate consumer-side typed clients from a compiled schema
435 ///
436 /// Distinct from `generate`: `generate` emits server-side authoring code (write
437 /// FraiseQL types in another language, fed back into the compiler), whereas
438 /// `generate-client` emits a typed client for callers of a FraiseQL API
439 /// (interfaces + query/mutation functions) from a compiled schema.compiled.json.
440 #[command(after_help = "\
441EXAMPLES:
442 fraiseql generate-client typescript --out ./src/generated
443 fraiseql generate-client typescript --schema ./schema.compiled.json --out ./gen --force
444 fraiseql generate-client python --out ./app/fraiseql_client
445 fraiseql generate-client go --out ./internal/fraiseqlclient
446 fraiseql generate-client rust --out ./src/generated")]
447 GenerateClient {
448 #[command(subcommand)]
449 language: GenerateClientCommands,
450 },
451
452 /// Initialize a new FraiseQL project
453 ///
454 /// Creates project directory with fraiseql.toml, schema.json,
455 /// database DDL structure, and authoring skeleton.
456 #[command(after_help = "\
457EXAMPLES:
458 fraiseql init my-app
459 fraiseql init my-app --language typescript --database postgres
460 fraiseql init my-app --size xs --no-git")]
461 Init {
462 /// Project name (used as directory name)
463 #[arg(value_name = "PROJECT_NAME")]
464 project_name: String,
465
466 /// Authoring language (python, typescript, rust, java, kotlin, go, csharp, swift, scala)
467 #[arg(short, long, default_value = "python")]
468 language: String,
469
470 /// Target database (postgres — FraiseQL is PostgreSQL-only since v2.15.0)
471 #[arg(long, default_value = "postgres")]
472 database: String,
473
474 /// Project size: xs (single file), s (flat dirs), m (per-entity dirs)
475 #[arg(long, default_value = "s")]
476 size: String,
477
478 /// Skip git init
479 #[arg(long)]
480 no_git: bool,
481 },
482
483 /// Run database migrations
484 ///
485 /// The long help is `commands::migrate::MIGRATE_LONG_ABOUT`: it states the order the
486 /// database URL is resolved in, so it lives beside the resolver.
487 #[command(long_about = crate::commands::migrate::MIGRATE_LONG_ABOUT)]
488 #[command(after_help = "\
489EXAMPLES:
490 fraiseql migrate up --database postgres://localhost/mydb
491 fraiseql migrate down --steps 1
492 fraiseql migrate status
493 fraiseql migrate create add_posts_table")]
494 Migrate {
495 #[command(subcommand)]
496 command: MigrateCommands,
497 },
498
499 /// Generate Software Bill of Materials
500 ///
501 /// Parses Cargo.lock and fraiseql.toml to produce a compliance-ready SBOM.
502 #[command(after_help = "\
503EXAMPLES:
504 fraiseql sbom
505 fraiseql sbom --format spdx
506 fraiseql sbom --format cyclonedx --output sbom.json")]
507 Sbom {
508 /// Output format (cyclonedx, spdx)
509 #[arg(short, long, default_value = "cyclonedx")]
510 format: String,
511
512 /// Output file path (default: stdout)
513 #[arg(short, long, value_name = "FILE")]
514 output: Option<String>,
515 },
516
517 /// List every optional capability and whether this build has it
518 ///
519 /// Reports the server this binary links (the one `fraiseql run` starts): each
520 /// capability's maturity, whether it is compiled in, its Cargo feature, its config
521 /// keys and compiled-schema sections, the smallest published artifact that ships it,
522 /// and its documentation page.
523 #[cfg(feature = "run-server")]
524 #[command(after_help = "\
525EXAMPLES:
526 fraiseql capabilities
527 fraiseql capabilities --json")]
528 Capabilities,
529
530 /// Compile schema and immediately start the GraphQL server
531 ///
532 /// Compiles the schema in-memory (no disk artifact) and starts the HTTP server.
533 /// With --watch, the server hot-reloads whenever the schema file changes.
534 ///
535 /// Server and database settings can be declared in fraiseql.toml under [server]
536 /// and [database] sections. CLI flags take precedence over TOML settings, which
537 /// take precedence over defaults. The database URL is resolved in this order:
538 /// --database flag > DATABASE_URL env var > [database].url in fraiseql.toml.
539 #[cfg(feature = "run-server")]
540 #[command(after_help = "\
541EXAMPLES:
542 fraiseql run
543 fraiseql run fraiseql.toml --database postgres://localhost/mydb
544 fraiseql run --port 3000 --watch
545 fraiseql run schema.json --introspection
546
547TOML CONFIG:
548 [server]
549 host = \"127.0.0.1\"
550 port = 9000
551
552 [server.cors]
553 origins = [\"https://app.example.com\"]
554
555 [database]
556 url = \"${DATABASE_URL}\"
557 pool_min = 2
558 pool_max = 20")]
559 Run {
560 /// Input file path (fraiseql.toml or schema.json); auto-detected if omitted
561 #[arg(value_name = "INPUT")]
562 input: Option<String>,
563
564 /// Database URL (overrides [database].url in fraiseql.toml and DATABASE_URL env var)
565 // Long-only: the global `--debug` (`Cli.debug`) already owns `-d`. A local
566 // `-d`/`--database` short here collides with it — clap's debug_asserts panic in
567 // debug builds and the short is ambiguous in release builds (#650).
568 #[arg(long, value_name = "DATABASE_URL")]
569 database: Option<String>,
570
571 /// Port to listen on (overrides [server].port in fraiseql.toml)
572 #[arg(short, long, value_name = "PORT")]
573 port: Option<u16>,
574
575 /// Bind address (overrides [server].host in fraiseql.toml)
576 #[arg(long, value_name = "HOST")]
577 bind: Option<String>,
578
579 /// Watch input file for changes and hot-reload the server
580 #[arg(short, long)]
581 watch: bool,
582
583 /// Enable the GraphQL introspection endpoint (no auth required)
584 #[arg(long)]
585 introspection: bool,
586 },
587
588 /// Watch a schema source and recompile + live-reload a running server on change
589 ///
590 /// Recompiles `schema.compiled.json` on every save and, with `--reload-url`,
591 /// POSTs it to a running server's `/api/v1/admin/reload-schema` endpoint for a
592 /// zero-downtime `ArcSwap` reload (no restart). Unlike `run --watch`, this drives
593 /// a server you started separately.
594 Watch {
595 /// Schema source to watch (fraiseql.toml or schema.json)
596 #[arg(value_name = "INPUT")]
597 input: String,
598
599 /// Compiled-schema output path (the server reads this on reload)
600 #[arg(short, long, default_value = "schema.compiled.json")]
601 output: String,
602
603 /// Base URL of a running server to live-reload (e.g. http://localhost:8080).
604 /// Omit to recompile to disk only.
605 #[arg(long, value_name = "URL")]
606 reload_url: Option<String>,
607
608 /// Bearer token for the server's admin reload endpoint
609 #[arg(long, value_name = "TOKEN")]
610 admin_token: Option<String>,
611
612 /// Database URL for compile-time validation (overrides DATABASE_URL)
613 #[arg(long, value_name = "DATABASE_URL")]
614 database: Option<String>,
615 },
616
617 /// Validate a trusted documents manifest
618 ///
619 /// Checks that the manifest JSON is well-formed and that each key
620 /// is a valid SHA-256 hex string matching its query body.
621 #[command(after_help = "\
622EXAMPLES:
623 fraiseql validate-documents manifest.json
624 fraiseql validate-documents manifest.json --max-cost 1000 --schema schema.compiled.json")]
625 ValidateDocuments {
626 /// Path to the trusted documents manifest JSON file
627 #[arg(value_name = "MANIFEST")]
628 manifest: String,
629
630 /// Fail validation when any document's estimated operation cost exceeds
631 /// this ceiling (#379). Costs are worst-case: a variable-valued
632 /// pagination argument is scored at its fail-closed ceiling.
633 #[arg(long, value_name = "COST")]
634 max_cost: Option<u64>,
635
636 /// Compiled schema (`schema.compiled.json`) supplying the
637 /// `[fraiseql.cost_weights]` used to score documents — the same numbers
638 /// the runtime enforces. Without it, cost equals the complexity score.
639 #[arg(long, value_name = "SCHEMA")]
640 schema: Option<String>,
641 },
642
643 /// Install FraiseQL mutation helper functions
644 ///
645 /// Installs SQL helper functions (fraiseql.mutation_ok, fraiseql.mutation_err, etc.)
646 /// to reduce boilerplate when writing mutation functions under the v2.4.0 protocol.
647 /// The helpers are installed in the `fraiseql` schema, which is owned by FraiseQL's
648 /// database role.
649 #[command(after_help = "\
650EXAMPLES:
651 fraiseql setup --database postgres://localhost/mydb
652 fraiseql setup --dry-run
653 fraiseql setup # Uses [database].url from fraiseql.toml, else DATABASE_URL")]
654 Setup {
655 /// Database connection URL (else [database].url in fraiseql.toml, else the
656 /// DATABASE_URL env var)
657 #[arg(long, value_name = "DATABASE_URL")]
658 database: Option<String>,
659
660 /// Print SQL without applying changes
661 #[arg(long)]
662 dry_run: bool,
663 },
664
665 /// Inspect schema metadata from a running FraiseQL server
666 ///
667 /// Fetches field-level security metadata (encryption, scope requirements, deny actions)
668 /// from the server's `/api/v1/schema/metadata` endpoint and displays it as a table.
669 #[command(after_help = "\
670EXAMPLES:
671 fraiseql schema metadata
672 fraiseql schema metadata --server http://localhost:8080
673 fraiseql schema metadata --server https://api.example.com --token mytoken")]
674 Schema {
675 #[command(subcommand)]
676 command: SchemaCommands,
677 },
678
679 /// Execute one GraphQL operation against a database and print the JSON result
680 ///
681 /// Boots the compiled schema in-process (no long-lived server, no HTTP layer)
682 /// against PostgreSQL, runs a single query or mutation, prints the GraphQL JSON
683 /// response to stdout, and exits. Exits non-zero on a resolution error — a
684 /// scriptable "does this actually resolve?" check for CI and the inner loop.
685 ///
686 /// Mutations COMMIT unless `--dry-run` is given, which runs the mutation inside
687 /// a transaction that is rolled back (validate-bind-without-commit).
688 #[command(after_help = "\
689EXAMPLES:
690 fraiseql query '{ orders(limit: 1) { id } }'
691 fraiseql query --schema schema.compiled.json --database postgres://localhost/db '{ users { id } }'
692 fraiseql query --variables '{\"id\": 42}' 'query($id: Int!) { user(id: $id) { id } }'
693 fraiseql query --dry-run 'mutation { createUser(name: \"x\") { id } }'")]
694 Query {
695 /// GraphQL operation to execute (a single query or mutation document).
696 #[arg(value_name = "QUERY")]
697 query: String,
698
699 /// Path to schema.compiled.json.
700 #[arg(short = 's', long, default_value = "schema.compiled.json")]
701 schema: std::path::PathBuf,
702
703 /// Database URL (PostgreSQL only). Falls back to the DATABASE_URL env var.
704 #[arg(long, value_name = "DATABASE_URL")]
705 database: Option<String>,
706
707 /// GraphQL variables as a JSON object string (e.g. '{"id": 42}').
708 #[arg(long, value_name = "JSON")]
709 variables: Option<String>,
710
711 /// Run mutations inside a rolled-back transaction: the function binds and
712 /// executes (constraints, triggers, and response shape are validated) but
713 /// nothing is committed. No effect on queries. PostgreSQL only.
714 #[arg(long)]
715 dry_run: bool,
716 },
717
718 /// Run diagnostic checks for common FraiseQL setup problems
719 ///
720 /// Checks schema file, TOML config, DATABASE_URL, JWT secret, Redis, TLS,
721 /// and cache/auth coherence. Prints a color-coded report and exits 0 if
722 /// all checks pass or 1 if any check fails (warnings do not fail).
723 #[command(after_help = "\
724EXAMPLES:
725 fraiseql doctor
726 fraiseql doctor --schema schema.compiled.json --config fraiseql.toml
727 fraiseql doctor --db-url postgres://user:pass@host:5432/db
728 fraiseql doctor --runtime --against-db postgres://user:pass@host:5432/db
729 fraiseql doctor --json")]
730 Doctor {
731 /// Path to fraiseql.toml configuration file.
732 #[arg(long, default_value = "fraiseql.toml")]
733 config: std::path::PathBuf,
734
735 /// Path to schema.compiled.json.
736 #[arg(long, default_value = "schema.compiled.json")]
737 schema: std::path::PathBuf,
738
739 /// Override DATABASE_URL for the connectivity check.
740 #[arg(long)]
741 db_url: Option<String>,
742
743 /// Run the live-database passes against this PostgreSQL database: the
744 /// change-log contract drift check (compares core.tb_entity_change_log
745 /// against the shipped contract — #380), the PL/pgSQL body-resolution
746 /// pass (reports internal calls that no longer resolve — #409), the
747 /// mutation→function contract drift check (every declared mutation has a
748 /// matching backing function with a mutation_response-shaped return —
749 /// #384), and the view-drift check (the same L1/L2/L3 view-composition
750 /// linter `compile --database` runs, reported as structured checks —
751 /// #384).
752 ///
753 /// The body-resolution pass requires the `plpgsql_check` extension and
754 /// skips with a hint when it is unavailable.
755 #[arg(long, value_name = "DATABASE_URL")]
756 against_db: Option<String>,
757
758 /// Runtime smoke mode (#501): boot the compiled schema in-process and
759 /// probe each root query field with a minimal selection, dry-running each
760 /// no-argument mutation (rolled back). Confirms operations actually
761 /// *resolve* end-to-end, complementing the static `--against-db` drift
762 /// checks. Requires a PostgreSQL URL via `--against-db` (or `--db-url`).
763 /// Queries / mutations that require arguments are skipped with a warning.
764 #[arg(long)]
765 runtime: bool,
766
767 /// Schemas to scan in the body-resolution pass (comma-separated).
768 #[arg(
769 long,
770 value_name = "SCHEMAS",
771 value_delimiter = ',',
772 default_value = "public"
773 )]
774 schemas: Vec<String>,
775
776 /// Output machine-readable JSON (for CI integration).
777 #[arg(long)]
778 json: bool,
779 },
780
781 /// Change-log performance observability (#392)
782 ///
783 /// Reads the framework-owned change-log (`core.v_entity_change_log`) and turns
784 /// it into operator-facing forensics: latency regressions between two time
785 /// windows, and ad-hoc reads of the slowest mutations / duration completeness
786 /// / per-operation percentiles.
787 Perf {
788 #[command(subcommand)]
789 command: PerfCommands,
790 },
791
792 /// List scheduled ingress sources and their durable cursor state (#573)
793 ///
794 /// Reads the compiled schema for each source's schedule and `run_as` ceiling,
795 /// and — when a database is reachable — its cursor value, version, and staleness
796 /// (seconds since the last advance). Leadership and last-fire are not reported
797 /// here (an advisory lock has no visible holder, and the lease is released
798 /// between ticks): watch `fraiseql_source_*` metrics and the poller logs for
799 /// those. Always exits 0 — it is a report.
800 #[command(after_help = "\
801EXAMPLES:
802 fraiseql sources
803 fraiseql sources --schema schema.compiled.json
804 fraiseql sources --db-url postgres://user:pass@host:5432/db
805 fraiseql sources --json")]
806 Sources {
807 /// Path to schema.compiled.json.
808 #[arg(long, default_value = "schema.compiled.json")]
809 schema: std::path::PathBuf,
810
811 /// PostgreSQL URL for cursor reads (defaults to `fraiseql.toml` /
812 /// `DATABASE_URL`). Without one, definitions are listed with no cursor state.
813 #[arg(long)]
814 db_url: Option<String>,
815
816 /// Output machine-readable JSON.
817 #[arg(long)]
818 json: bool,
819 },
820
821 /// Author-side function tooling (local test harness).
822 #[cfg(feature = "functions-invoke")]
823 #[command(subcommand)]
824 Functions(FunctionsCommands),
825}
826
827/// `fraiseql functions` subcommands.
828#[cfg(feature = "functions-invoke")]
829#[derive(Subcommand)]
830pub(crate) enum FunctionsCommands {
831 /// Run a compiled function in a real V8 isolate against a fixture payload,
832 /// with mocked host ops — the author's inner test loop.
833 ///
834 /// Exit codes: 0 = ran; 3 = the `when` predicate did not match (nothing would
835 /// fire); 4 = the guest errored; 1 = a config/harness error.
836 #[command(long_about = "\
837Run a compiled function against a fixture payload in a real V8 isolate.
838
839The module is loaded exactly as the server loads it (from the compiled schema's
840module_dir); host ops are mocked so no live server, database, or network is needed.
841Every host-op call the function makes is printed.
842
843EXAMPLES:
844 fraiseql functions invoke notifyApproved --payload event.json
845 fraiseql functions invoke notifyApproved --payload event.json --explain
846 fraiseql functions invoke syncDeal --payload deal.json \\
847 --mock-http http.json --mock-query query.json")]
848 Invoke {
849 /// The function name (matches a definition in the compiled schema).
850 name: String,
851
852 /// Path to the fixture payload JSON. For after:mutation / after:capture:
853 /// `{ \"event_kind\": \"update\", \"old\": {…}, \"new\": {…} }` (a bare
854 /// object is treated as an insert's `new` image).
855 #[arg(long)]
856 payload: std::path::PathBuf,
857
858 /// Path to schema.compiled.json.
859 #[arg(long, default_value = "schema.compiled.json")]
860 schema: std::path::PathBuf,
861
862 /// Canned HTTP responses (JSON array of `{url_contains, method?, status?, body?}`);
863 /// a request matching none fails loud.
864 #[arg(long)]
865 mock_http: Option<std::path::PathBuf>,
866
867 /// Canned query responses (JSON array of `{query_contains?, variables?, response}`)
868 /// for the `fraiseql_query` bridge. An entry with `variables` matches only a call
869 /// bound with exactly those variables; a query matching none fails loud.
870 #[arg(long)]
871 mock_query: Option<std::path::PathBuf>,
872
873 /// Inject a fixed idempotency token the guest reads via the host op.
874 #[arg(long)]
875 idempotency_token: Option<String>,
876
877 /// Print why the `when` predicates did or did not match this payload.
878 #[arg(long)]
879 explain: bool,
880 },
881}
882
883/// `fraiseql perf` subcommands.
884#[derive(Subcommand)]
885pub(crate) enum PerfCommands {
886 /// Flag mutations whose latency regressed between a baseline and a recent
887 /// window, per `(object_type, modification_type)`.
888 ///
889 /// Only rows carrying the current `duration_calc_version` are compared, and
890 /// latency is never aggregated across modification types — so a shift in the
891 /// operation mix cannot mask a regression as a false improvement. Exits 0 even
892 /// when regressions are found (it is a report); pass `--fail-on-regression`
893 /// for CI gating.
894 RegressionScan {
895 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
896 #[arg(long, value_name = "DATABASE_URL")]
897 database: Option<String>,
898
899 /// Recent window size, in days.
900 #[arg(long, default_value_t = 7)]
901 recent_days: i64,
902
903 /// Baseline window size (immediately before the recent window), in days.
904 #[arg(long, default_value_t = 7)]
905 baseline_days: i64,
906
907 /// Minimum comparable samples required on each side; thinner groups are
908 /// skipped rather than flagged.
909 #[arg(long, default_value_t = 20)]
910 min_samples: usize,
911
912 /// Minimum p50 increase to flag, in percent.
913 #[arg(long, default_value_t = 25.0)]
914 threshold_pct: f64,
915
916 /// Minimum absolute p50 increase to flag, in milliseconds (the noise floor).
917 #[arg(long, default_value_t = 5.0)]
918 min_delta_ms: f64,
919
920 /// Restrict the scan to a single object_type.
921 #[arg(long, value_name = "TYPE")]
922 object_type: Option<String>,
923
924 /// Exit non-zero when any regression is found (for CI gating).
925 #[arg(long)]
926 fail_on_regression: bool,
927 },
928
929 /// Ad-hoc forensic reads of the change-log.
930 Explore {
931 #[command(subcommand)]
932 command: PerfExploreCommands,
933 },
934}
935
936/// `fraiseql perf explore` subcommands — read-only views over comparable (v2)
937/// change-log rows in a trailing window.
938#[derive(Subcommand)]
939pub(crate) enum PerfExploreCommands {
940 /// The slowest individual mutations, slowest first.
941 Slowest {
942 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
943 #[arg(long, value_name = "DATABASE_URL")]
944 database: Option<String>,
945
946 /// Trailing window to scan, in days.
947 #[arg(long, default_value_t = 7)]
948 days: i32,
949
950 /// Restrict to a single object_type.
951 #[arg(long, value_name = "TYPE")]
952 object_type: Option<String>,
953
954 /// Maximum rows to return.
955 #[arg(long, default_value_t = 20)]
956 limit: usize,
957 },
958
959 /// `duration_ms` completeness (NULL / pre-fix rates), per operation.
960 NullRate {
961 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
962 #[arg(long, value_name = "DATABASE_URL")]
963 database: Option<String>,
964
965 /// Trailing window to scan, in days.
966 #[arg(long, default_value_t = 7)]
967 days: i32,
968
969 /// Restrict to a single object_type.
970 #[arg(long, value_name = "TYPE")]
971 object_type: Option<String>,
972 },
973
974 /// Latency percentiles (p50/p95/p99/max), per operation.
975 Summary {
976 /// PostgreSQL URL (defaults to `fraiseql.toml` / `DATABASE_URL`).
977 #[arg(long, value_name = "DATABASE_URL")]
978 database: Option<String>,
979
980 /// Trailing window to scan, in days.
981 #[arg(long, default_value_t = 7)]
982 days: i32,
983
984 /// Restrict to a single object_type.
985 #[arg(long, value_name = "TYPE")]
986 object_type: Option<String>,
987 },
988}
989
990#[derive(Subcommand)]
991pub(crate) enum ValidateCommands {
992 /// Validate that declared fact tables match database schema
993 Facts {
994 /// Schema.json file path
995 #[arg(short, long, value_name = "SCHEMA")]
996 schema: String,
997
998 /// Database connection string
999 // Long-only: `-d` is the global `--debug` short (#650).
1000 #[arg(long, value_name = "DATABASE_URL")]
1001 database: String,
1002 },
1003}
1004
1005#[derive(Subcommand)]
1006pub(crate) enum GenerateClientCommands {
1007 /// Generate a TypeScript client (interfaces + typed query/mutation functions).
1008 Typescript {
1009 /// Path to schema.compiled.json (auto-detected from conventional
1010 /// locations if omitted).
1011 #[arg(long, value_name = "SCHEMA")]
1012 schema: Option<std::path::PathBuf>,
1013
1014 /// Output directory for the generated client.
1015 #[arg(long, value_name = "DIR")]
1016 out: std::path::PathBuf,
1017
1018 /// Overwrite an existing generated client in the output directory.
1019 #[arg(long, default_value_t = false)]
1020 force: bool,
1021 },
1022
1023 /// Generate a Python client (TypedDicts + typed query/mutation functions;
1024 /// requires Python >= 3.12).
1025 Python {
1026 /// Path to schema.compiled.json (auto-detected from conventional
1027 /// locations if omitted).
1028 #[arg(long, value_name = "SCHEMA")]
1029 schema: Option<std::path::PathBuf>,
1030
1031 /// Output directory for the generated client (becomes a Python package).
1032 #[arg(long, value_name = "DIR")]
1033 out: std::path::PathBuf,
1034
1035 /// Overwrite an existing generated client in the output directory.
1036 #[arg(long, default_value_t = false)]
1037 force: bool,
1038 },
1039
1040 /// Generate a Go client (structs + typed query/mutation functions; requires
1041 /// Go >= 1.21 for the Relay `Connection[T]`).
1042 Go {
1043 /// Path to schema.compiled.json (auto-detected from conventional
1044 /// locations if omitted).
1045 #[arg(long, value_name = "SCHEMA")]
1046 schema: Option<std::path::PathBuf>,
1047
1048 /// Output directory for the generated client (becomes package
1049 /// `fraiseqlclient`).
1050 #[arg(long, value_name = "DIR")]
1051 out: std::path::PathBuf,
1052
1053 /// Overwrite an existing generated client in the output directory.
1054 #[arg(long, default_value_t = false)]
1055 force: bool,
1056 },
1057
1058 /// Generate a Rust client (serde types + typed query/mutation functions;
1059 /// depends only on serde and serde_json).
1060 Rust {
1061 /// Path to schema.compiled.json (auto-detected from conventional
1062 /// locations if omitted).
1063 #[arg(long, value_name = "SCHEMA")]
1064 schema: Option<std::path::PathBuf>,
1065
1066 /// Output directory for the generated client (becomes a module tree with
1067 /// its own `mod.rs`).
1068 #[arg(long, value_name = "DIR")]
1069 out: std::path::PathBuf,
1070
1071 /// Overwrite an existing generated client in the output directory.
1072 #[arg(long, default_value_t = false)]
1073 force: bool,
1074 },
1075}
1076
1077#[derive(Subcommand)]
1078pub(crate) enum FederationCommands {
1079 /// Draw the federation the given subgraphs form
1080 Graph {
1081 /// Each subgraph's schema.compiled.json
1082 #[arg(value_name = "SCHEMA", required = true, num_args = 1..)]
1083 schema: Vec<String>,
1084
1085 /// Output format (json, dot, mermaid)
1086 #[arg(short, long, value_name = "FORMAT", default_value = "json")]
1087 format: String,
1088 },
1089
1090 /// Print a subgraph's federation SDL, exactly as `_service { sdl }` serves it
1091 ///
1092 /// Reads the compiled schema; no server, database or credentials are needed.
1093 #[cfg(feature = "federation")]
1094 Sdl {
1095 /// Path to the subgraph's schema.compiled.json
1096 #[arg(value_name = "SCHEMA")]
1097 schema: std::path::PathBuf,
1098
1099 /// Write the SDL to this file instead of stdout
1100 #[arg(short, long, value_name = "FILE")]
1101 output: Option<std::path::PathBuf>,
1102 },
1103
1104 /// Validate a subgraph's federation entities and keys
1105 Check {
1106 /// Path to local schema.compiled.json
1107 #[arg(value_name = "SCHEMA")]
1108 schema: String,
1109
1110 /// Another subgraph's schema.compiled.json to compare with (@key agreement,
1111 /// field sharing)
1112 #[arg(short, long, value_name = "OTHER_SCHEMA")]
1113 against: Option<String>,
1114
1115 /// Output result as JSON
1116 #[arg(long)]
1117 json: bool,
1118 },
1119}
1120
1121#[derive(Subcommand)]
1122pub(crate) enum IntrospectCommands {
1123 /// Introspect database for fact tables (tf_* tables)
1124 Facts {
1125 /// Database connection string
1126 // Long-only: `-d` is the global `--debug` short (#650).
1127 #[arg(long, value_name = "DATABASE_URL")]
1128 database: String,
1129
1130 /// Output format (python, json)
1131 #[arg(short, long, value_name = "FORMAT", default_value = "python")]
1132 format: String,
1133 },
1134}
1135
1136#[derive(Subcommand)]
1137pub(crate) enum SchemaCommands {
1138 /// Display field-level security metadata from a running server
1139 Metadata {
1140 /// Server base URL
1141 #[arg(
1142 short,
1143 long,
1144 value_name = "URL",
1145 default_value = "http://localhost:8080"
1146 )]
1147 server: String,
1148
1149 /// Bearer token for authentication
1150 #[arg(short, long, value_name = "TOKEN")]
1151 token: Option<String>,
1152 },
1153}
1154
1155#[derive(Subcommand)]
1156pub(crate) enum MigrateCommands {
1157 /// Apply pending migrations
1158 Up {
1159 /// Database connection URL
1160 #[arg(long, value_name = "DATABASE_URL")]
1161 database: Option<String>,
1162
1163 /// Migration directory
1164 #[arg(long, value_name = "DIR")]
1165 dir: Option<String>,
1166 },
1167
1168 /// Roll back migrations
1169 Down {
1170 /// Database connection URL
1171 #[arg(long, value_name = "DATABASE_URL")]
1172 database: Option<String>,
1173
1174 /// Migration directory
1175 #[arg(long, value_name = "DIR")]
1176 dir: Option<String>,
1177
1178 /// Number of migrations to roll back
1179 #[arg(long, default_value = "1")]
1180 steps: u32,
1181 },
1182
1183 /// Show migration status
1184 Status {
1185 /// Database connection URL
1186 #[arg(long, value_name = "DATABASE_URL")]
1187 database: Option<String>,
1188
1189 /// Migration directory
1190 #[arg(long, value_name = "DIR")]
1191 dir: Option<String>,
1192 },
1193
1194 /// Create a new migration file
1195 ///
1196 /// Delegates to `confiture migrate generate`, like `generate`; kept as the
1197 /// verb the examples above use.
1198 Create {
1199 /// Migration name
1200 #[arg(value_name = "NAME")]
1201 name: String,
1202
1203 /// Migration directory
1204 #[arg(long, value_name = "DIR")]
1205 dir: Option<String>,
1206 },
1207
1208 /// Generate a new migration from schema diff
1209 ///
1210 /// Delegates to `confiture migrate generate`.
1211 /// Creates a timestamped migration file.
1212 Generate {
1213 /// Migration name
1214 #[arg(value_name = "NAME")]
1215 name: String,
1216
1217 /// Migration directory
1218 #[arg(long, value_name = "DIR")]
1219 dir: Option<String>,
1220 },
1221
1222 /// Validate migration files for naming, idempotency, and drift
1223 ///
1224 /// Delegates to `confiture migrate validate`.
1225 /// Checks naming conventions, idempotency, and schema drift.
1226 Validate {
1227 /// Migration directory
1228 #[arg(long, value_name = "DIR")]
1229 dir: Option<String>,
1230 },
1231
1232 /// Pre-deploy safety check on pending migrations
1233 ///
1234 /// Delegates to `confiture migrate preflight`.
1235 /// Verifies reversibility, detects non-transactional statements, checks checksums.
1236 Preflight {
1237 /// Migration directory
1238 #[arg(long, value_name = "DIR")]
1239 dir: Option<String>,
1240 },
1241}