use std::{path::Path, sync::Arc};
use anyhow::{Context, Result, bail};
use fraiseql_core::{
db::postgres::PostgresAdapter,
runtime::{Executor, RuntimeConfig},
schema::CompiledSchema,
};
pub async fn run(
query: &str,
schema_path: &Path,
database: Option<String>,
variables: Option<String>,
dry_run: bool,
) -> Result<()> {
let schema = load_schema(schema_path)?;
let db_url = resolve_db_url(database)?;
crate::connection::require_postgres(&db_url)?;
let variables = parse_variables(variables.as_deref())?;
if !dry_run && is_mutation(query) {
eprintln!(
"warning: executing a mutation — changes will be COMMITTED. \
Pass --dry-run to validate the binding without committing."
);
}
let adapter =
Arc::new(PostgresAdapter::new(&db_url).await.context("failed to connect to PostgreSQL")?);
let config = RuntimeConfig {
dry_run_mutations: dry_run,
..RuntimeConfig::default()
};
let executor = Executor::with_config(schema, adapter, config);
let result = executor.execute(query, variables.as_ref()).await.context("execution failed")?;
println!(
"{}",
serde_json::to_string_pretty(&result).context("failed to serialize result")?
);
if has_errors(&result) {
std::process::exit(1);
}
Ok(())
}
fn load_schema(path: &Path) -> Result<CompiledSchema> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("failed to read schema file {}", path.display()))?;
CompiledSchema::from_json(&text, false)
.with_context(|| format!("failed to parse compiled schema {}", path.display()))
}
fn resolve_db_url(cli: Option<String>) -> Result<String> {
cli.or_else(|| std::env::var("DATABASE_URL").ok()).ok_or_else(|| {
anyhow::anyhow!(
"No database URL provided. Pass --database or set the DATABASE_URL env var."
)
})
}
pub(crate) fn parse_variables(raw: Option<&str>) -> Result<Option<serde_json::Value>> {
match raw {
None => Ok(None),
Some(s) => {
let value: serde_json::Value =
serde_json::from_str(s).context("--variables is not valid JSON")?;
if !value.is_object() {
bail!("--variables must be a JSON object (e.g. '{{\"id\": 42}}').");
}
Ok(Some(value))
},
}
}
pub(crate) fn has_errors(result: &serde_json::Value) -> bool {
result
.get("errors")
.and_then(serde_json::Value::as_array)
.is_some_and(|a| !a.is_empty())
}
fn is_mutation(query: &str) -> bool {
fraiseql_core::graphql::parse_query(query).is_ok_and(|p| p.operation_type == "mutation")
}