fraiseql_cli/commands/
generate_client.rs1use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use fraiseql_core::schema::CompiledSchema;
14
15#[derive(Debug, Clone, Copy)]
17pub enum ClientLanguage {
18 TypeScript,
20}
21
22pub 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
81fn 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
89fn 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}