fraiseql-cli 2.4.0

CLI tools for FraiseQL v2 - Schema compilation and development utilities
Documentation
//! `fraiseql generate-client` — generate consumer-side typed clients from a
//! compiled schema (`schema.compiled.json`).
//!
//! This is distinct from `fraiseql generate`, which emits server-side **authoring**
//! code (FraiseQL type definitions in another language, fed back into the
//! compiler). `generate-client` consumes the compiler's *output* to build a client
//! that *calls* the API. The generation itself lives in the `fraiseql-codegen`
//! crate; this command is the thin filesystem wrapper.

use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use fraiseql_core::schema::CompiledSchema;

/// Supported client target languages.
#[derive(Debug, Clone, Copy)]
pub enum ClientLanguage {
    /// TypeScript client (interfaces + typed query/mutation functions).
    TypeScript,
}

/// Run `generate-client <language>`.
///
/// `schema_path` is auto-detected from conventional locations when `None`.
/// Refuses to overwrite an existing generated tree unless `force` is set.
///
/// # Errors
///
/// Returns an error if the schema cannot be found, read, or parsed, if the output
/// directory already contains a generated client and `force` is not set, or if any
/// file cannot be written.
pub fn run(
    language: ClientLanguage,
    schema_path: Option<&Path>,
    out_dir: &Path,
    force: bool,
) -> Result<()> {
    let schema_path = match schema_path {
        Some(p) => p.to_path_buf(),
        None => auto_detect_compiled_schema()?,
    };

    let raw = std::fs::read_to_string(&schema_path)
        .with_context(|| format!("Failed to read compiled schema {}", schema_path.display()))?;
    let schema: CompiledSchema = serde_json::from_str(&raw).with_context(|| {
        format!("Failed to parse {} as a compiled schema", schema_path.display())
    })?;

    let files = match language {
        ClientLanguage::TypeScript => fraiseql_codegen::client::typescript::generate(&schema)
            .map_err(|e| anyhow::anyhow!("client generation failed: {e}"))?,
    };

    if out_dir.exists() && !force && contains_generated_client(out_dir) {
        anyhow::bail!(
            "Output directory {} already contains a generated client. Pass --force to overwrite.",
            out_dir.display()
        );
    }

    for (rel_path, content) in &files {
        let full_path = out_dir.join(rel_path);
        if let Some(parent) = full_path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create {}", parent.display()))?;
        }
        std::fs::write(&full_path, content)
            .with_context(|| format!("Failed to write {}", full_path.display()))?;
        println!("  wrote {}", full_path.display());
    }

    println!(
        "\nGenerated {} files into {}. Type-check with `tsc --strict --noEmit {}/index.ts`.",
        files.len(),
        out_dir.display(),
        out_dir.display(),
    );
    Ok(())
}

/// Whether a directory already holds a generated client (detected via the
/// auto-generated sentinel in `types.ts`).
fn contains_generated_client(out_dir: &Path) -> bool {
    let marker = out_dir.join("types.ts");
    std::fs::read_to_string(marker)
        .is_ok_and(|content| content.contains("AUTO-GENERATED by fraiseql-codegen"))
}

/// Search conventional locations for a `schema.compiled.json`.
fn auto_detect_compiled_schema() -> Result<PathBuf> {
    let candidates = [
        "schema.compiled.json",
        "target/fraiseql/schema.compiled.json",
        "build/schema.compiled.json",
    ];
    for candidate in candidates {
        let path = PathBuf::from(candidate);
        if path.exists() {
            return Ok(path);
        }
    }
    anyhow::bail!(
        "No compiled schema found. Compile first (`fraiseql compile`) or pass an explicit path: \
         fraiseql generate-client typescript --schema <path> --out <dir>"
    )
}