Skip to main content

fraiseql_cli/commands/
generate_client.rs

1//! `fraiseql generate-client` — generate consumer-side typed clients from a
2//! compiled schema (`schema.compiled.json`).
3//!
4//! This is distinct from `fraiseql generate`, which emits server-side **authoring**
5//! code (FraiseQL type definitions in another language, fed back into the
6//! compiler). `generate-client` consumes the compiler's *output* to build a client
7//! that *calls* the API. The generation itself lives in the `fraiseql-codegen`
8//! crate; this command is the thin filesystem wrapper.
9
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use fraiseql_core::schema::CompiledSchema;
14
15/// Supported client target languages.
16#[derive(Debug, Clone, Copy)]
17pub enum ClientLanguage {
18    /// TypeScript client (interfaces + typed query/mutation functions).
19    TypeScript,
20}
21
22/// Run `generate-client <language>`.
23///
24/// `schema_path` is auto-detected from conventional locations when `None`.
25/// Refuses to overwrite an existing generated tree unless `force` is set.
26///
27/// # Errors
28///
29/// Returns an error if the schema cannot be found, read, or parsed, if the output
30/// directory already contains a generated client and `force` is not set, or if any
31/// file cannot be written.
32pub fn run(
33    language: ClientLanguage,
34    schema_path: Option<&Path>,
35    out_dir: &Path,
36    force: bool,
37) -> Result<()> {
38    let schema_path = match schema_path {
39        Some(p) => p.to_path_buf(),
40        None => auto_detect_compiled_schema()?,
41    };
42
43    let raw = std::fs::read_to_string(&schema_path)
44        .with_context(|| format!("Failed to read compiled schema {}", schema_path.display()))?;
45    let schema: CompiledSchema = serde_json::from_str(&raw).with_context(|| {
46        format!("Failed to parse {} as a compiled schema", schema_path.display())
47    })?;
48
49    let files = match language {
50        ClientLanguage::TypeScript => fraiseql_codegen::client::typescript::generate(&schema)
51            .map_err(|e| anyhow::anyhow!("client generation failed: {e}"))?,
52    };
53
54    if out_dir.exists() && !force && contains_generated_client(out_dir) {
55        anyhow::bail!(
56            "Output directory {} already contains a generated client. Pass --force to overwrite.",
57            out_dir.display()
58        );
59    }
60
61    for (rel_path, content) in &files {
62        let full_path = out_dir.join(rel_path);
63        if let Some(parent) = full_path.parent() {
64            std::fs::create_dir_all(parent)
65                .with_context(|| format!("Failed to create {}", parent.display()))?;
66        }
67        std::fs::write(&full_path, content)
68            .with_context(|| format!("Failed to write {}", full_path.display()))?;
69        println!("  wrote {}", full_path.display());
70    }
71
72    println!(
73        "\nGenerated {} files into {}. Type-check with `tsc --strict --noEmit {}/index.ts`.",
74        files.len(),
75        out_dir.display(),
76        out_dir.display(),
77    );
78    Ok(())
79}
80
81/// Whether a directory already holds a generated client (detected via the
82/// auto-generated sentinel in `types.ts`).
83fn contains_generated_client(out_dir: &Path) -> bool {
84    let marker = out_dir.join("types.ts");
85    std::fs::read_to_string(marker)
86        .is_ok_and(|content| content.contains("AUTO-GENERATED by fraiseql-codegen"))
87}
88
89/// Search conventional locations for a `schema.compiled.json`.
90fn auto_detect_compiled_schema() -> Result<PathBuf> {
91    let candidates = [
92        "schema.compiled.json",
93        "target/fraiseql/schema.compiled.json",
94        "build/schema.compiled.json",
95    ];
96    for candidate in candidates {
97        let path = PathBuf::from(candidate);
98        if path.exists() {
99            return Ok(path);
100        }
101    }
102    anyhow::bail!(
103        "No compiled schema found. Compile first (`fraiseql compile`) or pass an explicit path: \
104         fraiseql generate-client typescript --schema <path> --out <dir>"
105    )
106}