use crate::automation::callable::ExecutionContext;
use crate::automation::{EventHook, EventHookEvent, ScheduledJob, Scheduler};
use crate::engine::Database;
use crate::error::DbxResult;
use std::sync::{Arc, RwLock};
pub struct TriggerRegistry {
triggers: RwLock<Vec<Arc<EventHook>>>,
}
impl TriggerRegistry {
pub fn new() -> Self {
Self {
triggers: RwLock::new(Vec::new()),
}
}
pub fn register(&self, hook: Arc<EventHook>) -> DbxResult<()> {
let mut triggers = self
.triggers
.write()
.map_err(|_| crate::error::DbxError::LockPoisoned)?;
if triggers.iter().any(|t| t.name() == hook.name()) {
return Err(crate::error::DbxError::DuplicateCallable(
hook.name().to_string(),
));
}
triggers.push(hook);
Ok(())
}
pub fn unregister(&self, name: &str) -> DbxResult<()> {
let mut triggers = self
.triggers
.write()
.map_err(|_| crate::error::DbxError::LockPoisoned)?;
let pos = triggers
.iter()
.position(|t| t.name() == name)
.ok_or_else(|| crate::error::DbxError::CallableNotFound(name.to_string()))?;
triggers.remove(pos);
Ok(())
}
pub fn fire(&self, ctx: &ExecutionContext, event: &EventHookEvent) -> DbxResult<Vec<String>> {
let triggers = self
.triggers
.read()
.map_err(|_| crate::error::DbxError::LockPoisoned)?;
let mut executed = Vec::new();
for hook in triggers.iter() {
match hook.fire(ctx, event) {
Ok(true) => executed.push(hook.name().to_string()),
Ok(false) => {} Err(e) => {
eprintln!("[EVENT HOOK ERROR] {}: {}", hook.name(), e);
}
}
}
Ok(executed)
}
pub fn list(&self) -> DbxResult<Vec<String>> {
let triggers = self
.triggers
.read()
.map_err(|_| crate::error::DbxError::LockPoisoned)?;
Ok(triggers.iter().map(|t| t.name().to_string()).collect())
}
}
impl Default for TriggerRegistry {
fn default() -> Self {
Self::new()
}
}
impl Database {
pub fn register_trigger(&self, hook: EventHook) -> DbxResult<()> {
let hook = Arc::new(hook);
self.automation_engine
.register(Arc::clone(&hook) as Arc<dyn crate::automation::callable::Callable>)?;
self.trigger_registry.register(hook)
}
pub fn unregister_trigger(&self, name: &str) -> DbxResult<()> {
self.automation_engine.unregister(name)?;
self.trigger_registry.unregister(name)
}
pub fn fire_trigger(&self, event: EventHookEvent) -> DbxResult<Vec<String>> {
let ctx = ExecutionContext::new(Arc::new(Database::open_in_memory()?));
self.trigger_registry.fire(&ctx, &event)
}
pub fn fire_trigger_with_ctx(
&self,
ctx: &ExecutionContext,
event: EventHookEvent,
) -> DbxResult<Vec<String>> {
self.trigger_registry.fire(ctx, &event)
}
pub fn list_triggers(&self) -> DbxResult<Vec<String>> {
self.trigger_registry.list()
}
pub fn create_scheduler(&self) -> Scheduler {
Scheduler::new(Arc::clone(&self.automation_engine))
}
pub fn register_scheduled_job(
&self,
scheduler: &Scheduler,
job: ScheduledJob,
) -> DbxResult<()> {
scheduler.register(job)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::automation::callable::{DataType, Signature, Value};
use crate::automation::event_hook::{
EventHook, EventHookAction, EventHookCondition, EventHookEvent, EventHookEventType,
};
use crate::automation::scheduler::{Schedule, ScheduleType, ScheduledJob};
use std::sync::Mutex;
use std::time::Duration;
#[test]
fn test_register_trigger() {
let db = Database::open_in_memory().unwrap();
let executed = Arc::new(Mutex::new(false));
let executed_clone = Arc::clone(&executed);
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(move |_ctx, _event| {
*executed_clone.lock().unwrap() = true;
Ok(())
})),
);
db.register_trigger(hook).unwrap();
let triggers = db.list_triggers().unwrap();
assert_eq!(triggers.len(), 1);
assert!(triggers.contains(&"test_trigger".to_string()));
}
#[test]
fn test_unregister_trigger() {
let db = Database::open_in_memory().unwrap();
let hook = EventHook::new(
"test_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(|_ctx, _event| Ok(()))),
);
db.register_trigger(hook).unwrap();
assert_eq!(db.list_triggers().unwrap().len(), 1);
db.unregister_trigger("test_trigger").unwrap();
assert_eq!(db.list_triggers().unwrap().len(), 0);
}
#[test]
fn test_create_scheduler() {
let db = Database::open_in_memory().unwrap();
let scheduler = db.create_scheduler();
let schedule = Schedule::new(ScheduleType::Interval(Duration::from_secs(60)));
let job = ScheduledJob::new("test_job", schedule, "test_udf", vec![]);
scheduler.register(job).unwrap();
let jobs = scheduler.list().unwrap();
assert_eq!(jobs.len(), 1);
}
#[test]
fn test_trigger_with_udf() {
let db = Database::open_in_memory().unwrap();
db.register_scalar_udf(
"double",
Signature {
params: vec![DataType::Int],
return_type: DataType::Int,
is_variadic: false,
},
|args| {
let x = args[0].as_i64()?;
Ok(Value::Int(x * 2))
},
)
.unwrap();
let result = Arc::new(Mutex::new(0i64));
let result_clone = Arc::clone(&result);
let hook = EventHook::new(
"double_trigger",
EventHookEventType::AfterInsert,
"users",
EventHookCondition::Always,
EventHookAction::Custom(Box::new(move |ctx, event| {
if let Some(value) = event.data.get("id") {
let doubled = ctx.dbx.call_udf("double", std::slice::from_ref(value))?;
*result_clone.lock().unwrap() = doubled.as_i64()?;
}
Ok(())
})),
);
db.register_trigger(hook).unwrap();
let event = EventHookEvent::new(EventHookEventType::AfterInsert, "users")
.with_data("id", Value::Int(21));
db.fire_trigger(event).unwrap();
let callables = db.list_triggers().unwrap();
assert!(!callables.is_empty()); assert!(callables.contains(&"double_trigger".to_string()));
}
#[test]
fn test_scheduler_with_udf() {
let db = Database::open_in_memory().unwrap();
db.register_scalar_udf(
"triple",
Signature {
params: vec![DataType::Int],
return_type: DataType::Int,
is_variadic: false,
},
|args| {
let x = args[0].as_i64()?;
Ok(Value::Int(x * 3))
},
)
.unwrap();
let scheduler = db.create_scheduler();
let schedule = Schedule::new(ScheduleType::Once(Duration::from_secs(0)));
let job = ScheduledJob::new("triple_job", schedule, "triple", vec![Value::Int(14)]);
scheduler.register(job).unwrap();
let jobs = scheduler.list().unwrap();
assert_eq!(jobs.len(), 1);
assert!(jobs.contains(&"triple_job".to_string()));
}
}