Skip to main content

fraiseql_cli/commands/
serve.rs

1//! Development server command with hot-reload
2//!
3//! Watches schema.json for changes and auto-recompiles
4
5use std::{path::Path, sync::mpsc::channel, time::Duration};
6
7use anyhow::{Context, Result};
8use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
9use tracing::{error, info};
10
11/// Run the serve command (development server with hot-reload)
12///
13/// # Arguments
14///
15/// * `schema` - Path to schema.json file to watch
16/// * `port` - Port to listen on (for future GraphQL server integration)
17///
18/// # Behavior
19///
20/// 1. Compiles the initial schema
21/// 2. Watches schema.json for file changes
22/// 3. Auto-recompiles on save
23/// 4. Provides compilation feedback
24///
25/// # Errors
26///
27/// Returns an error if the schema file does not exist, if the file watcher cannot
28/// be created or started, or if the watch channel closes unexpectedly.
29///
30/// # Future Enhancement
31///
32/// Will integrate with fraiseql-server to provide hot-reload of the GraphQL endpoint
33#[allow(clippy::cognitive_complexity)] // Reason: server startup with config loading, adapter setup, and hot-reload wiring
34pub async fn run(schema: &str, port: u16) -> Result<()> {
35    info!("Starting development server");
36    println!("🚀 FraiseQL Dev Server");
37    println!("   Schema: {schema}");
38    println!("   Port:   {port} (GraphQL server integration coming soon)");
39    println!("   Watching for changes...\n");
40
41    // Verify schema file exists
42    let schema_path = Path::new(schema);
43    if !schema_path.exists() {
44        anyhow::bail!("Schema file not found: {schema}");
45    }
46
47    // Compile initial schema
48    println!("Initial compilation:");
49    match compile_schema(schema).await {
50        Ok(()) => println!("   ✓ Schema compiled successfully\n"),
51        Err(e) => {
52            error!("Initial compilation failed: {e}");
53            println!("   err: Compilation failed: {e}\n");
54            println!("   Fix errors and save to retry...\n");
55        },
56    }
57
58    // Set up file watcher
59    let (tx, rx) = channel();
60
61    let mut watcher = RecommendedWatcher::new(
62        move |res: Result<Event, notify::Error>| {
63            if let Ok(event) = res {
64                let _ = tx.send(event);
65            }
66        },
67        Config::default(),
68    )
69    .context("Failed to create file watcher")?;
70
71    // Watch the schema file
72    watcher
73        .watch(schema_path, RecursiveMode::NonRecursive)
74        .context("Failed to watch schema file")?;
75
76    // Watch for file changes
77    loop {
78        match rx.recv() {
79            Ok(event) => {
80                // Only recompile on write/modify events
81                if matches!(event.kind, EventKind::Modify(_)) {
82                    info!("Schema file modified, recompiling...");
83                    println!("Schema changed, recompiling...");
84
85                    // Small delay to ensure file write is complete
86                    tokio::time::sleep(Duration::from_millis(100)).await;
87
88                    match compile_schema(schema).await {
89                        Ok(()) => {
90                            info!("Recompilation successful");
91                            println!("   ✓ Recompiled successfully\n");
92                        },
93                        Err(e) => {
94                            error!("Recompilation failed: {e}");
95                            println!("   err: Compilation failed: {e}\n");
96                        },
97                    }
98                }
99            },
100            Err(e) => {
101                error!("Watch error: {e}");
102                anyhow::bail!("File watch error: {e}");
103            },
104        }
105    }
106}
107
108/// Compile schema (used by file watcher)
109async fn compile_schema(input: &str) -> Result<()> {
110    let output = input.replace(".json", ".compiled.json");
111
112    // Use the compile command logic (no database validation for dev server)
113    super::compile::run(
114        input,
115        None,
116        None,
117        Vec::new(),
118        Vec::new(),
119        Vec::new(),
120        &output,
121        false,
122        None,
123        None,
124        false,
125        false,
126    )
127    .await
128}