use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::types::EventPayload;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EventKind {
Insert,
Update,
Delete,
}
impl EventKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
EventKind::Insert => "insert",
EventKind::Update => "update",
EventKind::Delete => "delete",
}
}
}
impl std::fmt::Display for EventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct EntityEvent {
pub entity: String,
pub event_kind: EventKind,
pub old: Option<serde_json::Value>,
pub new: Option<serde_json::Value>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct AfterMutationTrigger {
pub function_name: String,
pub entity_type: String,
pub event_filter: Option<EventKind>,
}
impl AfterMutationTrigger {
#[must_use]
pub fn matches(&self, entity: &str, event_kind: EventKind) -> bool {
self.entity_type == entity && self.event_filter.is_none_or(|filter| filter == event_kind)
}
#[must_use]
pub fn build_payload(&self, event: &EntityEvent) -> EventPayload {
EventPayload {
trigger_type: format!("after:mutation:{}", self.function_name),
entity: event.entity.clone(),
event_kind: event.event_kind.to_string(),
data: serde_json::json!({
"event_kind": event.event_kind.as_str(),
"old": event.old,
"new": event.new,
}),
timestamp: event.timestamp,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BeforeMutationResult {
Proceed(serde_json::Value),
Abort(String),
}
#[derive(Debug, Clone)]
pub struct BeforeMutationTrigger {
pub function_name: String,
pub mutation_name: String,
}
impl BeforeMutationTrigger {
#[must_use]
pub fn matches(&self, mutation: &str) -> bool {
self.mutation_name == mutation
}
}
#[derive(Debug, Clone)]
pub struct BeforeMutationChain {
pub triggers: Vec<BeforeMutationTrigger>,
}
impl BeforeMutationChain {
pub async fn execute<H>(
&self,
input: serde_json::Value,
modules: &std::collections::HashMap<String, crate::types::FunctionModule>,
observer: &crate::observer::FunctionObserver,
host: &H,
limits: crate::types::ResourceLimits,
) -> fraiseql_error::Result<BeforeMutationResult>
where
H: crate::HostContext + ?Sized,
{
let mut current = input;
for trigger in &self.triggers {
let module = modules.get(&trigger.function_name).ok_or_else(|| {
fraiseql_error::FraiseQLError::Validation {
message: format!(
"before:mutation function '{}' not found in module registry",
trigger.function_name,
),
path: None,
}
})?;
let payload = crate::types::EventPayload {
trigger_type: format!("before:mutation:{}", trigger.mutation_name),
entity: trigger.mutation_name.clone(),
event_kind: "before".to_string(),
data: current.clone(),
timestamp: chrono::Utc::now(),
};
let result = observer.invoke(module, payload, host, limits.clone()).await?;
match result.value {
Some(ref v) if v.get("abort").is_some() => {
let msg = v["abort"]
.as_str()
.unwrap_or("Aborted by before:mutation trigger")
.to_string();
return Ok(BeforeMutationResult::Abort(msg));
},
Some(ref v) if v.get("input").is_some() => {
current = v["input"].clone();
},
_ => {},
}
}
Ok(BeforeMutationResult::Proceed(current))
}
}
#[derive(Debug, Clone)]
pub struct TriggerMatcher {
specific: HashMap<String, HashMap<String, Vec<AfterMutationTrigger>>>,
all_kinds: HashMap<String, Vec<AfterMutationTrigger>>,
}
impl TriggerMatcher {
#[must_use]
pub fn new() -> Self {
Self {
specific: HashMap::new(),
all_kinds: HashMap::new(),
}
}
pub fn add(&mut self, trigger: AfterMutationTrigger) {
match trigger.event_filter {
Some(event_kind) => {
self.specific
.entry(trigger.entity_type.clone())
.or_default()
.entry(event_kind.as_str().to_string())
.or_default()
.push(trigger);
},
None => {
self.all_kinds.entry(trigger.entity_type.clone()).or_default().push(trigger);
},
}
}
#[must_use]
pub fn find(&self, entity: &str, event_kind: EventKind) -> Vec<AfterMutationTrigger> {
let event_str = event_kind.as_str();
let mut result = Vec::new();
if let Some(entity_map) = self.specific.get(entity) {
if let Some(triggers) = entity_map.get(event_str) {
result.extend(triggers.clone());
}
}
if let Some(triggers) = self.all_kinds.get(entity) {
result.extend(triggers.clone());
}
result
}
}
impl Default for TriggerMatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests;