use anyhow::{Result, bail};
use fraiseql_core::schema::ObserversBackend;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const REDIS_URL_REFUSED: &str = "observers `redis_url` configures nothing: the observer \
runtime has no Redis transport. Remove it, and select the transport in fraiseql.toml \
[observers] with backend = \"postgres\" (the default) or backend = \"nats\" and nats_url";
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ObserversConfig {
#[serde(default)]
pub enabled: bool,
pub backend: ObserversBackend,
#[serde(default, skip_serializing)]
pub redis_url: Option<toml::Value>,
pub nats_url: Option<String>,
pub handlers: Vec<EventHandler>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime: Option<toml::Value>,
}
impl ObserversConfig {
#[must_use]
pub const fn is_configured(&self) -> bool {
self.enabled || self.nats_url.is_some()
}
#[must_use]
pub fn to_compiled(&self) -> fraiseql_core::schema::ObserversConfig {
fraiseql_core::schema::ObserversConfig {
enabled: self.enabled,
backend: self.backend,
nats_url: self.nats_url.clone(),
}
}
pub fn refuse_handlers(&self) -> Result<()> {
if !self.handlers.is_empty() {
bail!(
"[[observers.handlers]] is not supported: compiled TOML handlers are not a \
runtime concept (#631) — runtime observers come only from the `tb_observer` \
table and the admin observer API. Define observers in `tb_observer` / via \
`POST /api/observers` and remove the [[observers.handlers]] block."
);
}
Ok(())
}
pub fn refuse_redis_url(&self) -> Result<()> {
if self.redis_url.is_some() {
bail!("{REDIS_URL_REFUSED}");
}
Ok(())
}
pub fn reconcile(table: Option<&Self>, document: Option<Value>) -> Result<Option<Value>> {
let Some(table) = table else {
return Ok(document);
};
table.refuse_handlers()?;
table.refuse_redis_url()?;
let from_config = table.to_compiled();
let Some(document) = document else {
return Ok(Some(serde_json::to_value(&from_config)?));
};
let mut settings = document.clone();
if let Some(map) = settings.as_object_mut() {
map.remove("handlers");
}
let Ok(from_document) =
serde_json::from_value::<fraiseql_core::schema::ObserversConfig>(settings)
else {
return Ok(Some(document));
};
let differing = [
("enabled", from_config.enabled != from_document.enabled),
("backend", from_config.backend != from_document.backend),
("nats_url", from_config.nats_url != from_document.nats_url),
]
.into_iter()
.find_map(|(key, differs)| differs.then_some(key));
if let Some(key) = differing {
bail!(
"fraiseql.toml [observers] and schema.json observers_config disagree on \
`{key}`.\n fraiseql.toml [observers]: {}\n schema.json observers_config: \
{}\nDeclare the observers settings once, in fraiseql.toml.",
serde_json::to_string(&from_config).unwrap_or_default(),
serde_json::to_string(&from_document).unwrap_or_default(),
);
}
Ok(Some(document))
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct EventHandler {
pub name: String,
pub event: String,
pub action: String,
pub webhook_url: Option<String>,
pub retry_strategy: Option<String>,
pub max_retries: Option<u32>,
pub description: Option<String>,
}