//! Trigger and Sensor repository for database operations //! //! This module provides CRUD operations and queries for Trigger and Sensor entities. use crate::models::{trigger::*, Id, JsonSchema}; use crate::Result; use serde_json::Value as JsonValue; use sqlx::{Executor, Postgres, QueryBuilder}; use super::{Create, Delete, FindById, FindByRef, List, Patch, Repository, Update}; // ============================================================================ // Trigger Search // ============================================================================ /// Filters for [`TriggerRepository::list_search`]. /// /// All fields are optional and combinable (AND). Pagination is always applied. #[derive(Debug, Clone, Default)] pub struct TriggerSearchFilters { /// Filter by pack ID pub pack: Option, /// Filter by enabled status pub enabled: Option, pub limit: u32, pub offset: u32, } /// Result of [`TriggerRepository::list_search`]. #[derive(Debug)] pub struct TriggerSearchResult { pub rows: Vec, pub total: u64, } // ============================================================================ // Sensor Search // ============================================================================ /// Filters for [`SensorRepository::list_search`]. /// /// All fields are optional and combinable (AND). Pagination is always applied. #[derive(Debug, Clone, Default)] pub struct SensorSearchFilters { /// Filter by pack ID pub pack: Option, /// Filter by trigger ID pub trigger: Option, /// Filter by enabled status pub enabled: Option, pub limit: u32, pub offset: u32, } /// Result of [`SensorRepository::list_search`]. #[derive(Debug)] pub struct SensorSearchResult { pub rows: Vec, pub total: u64, } /// Repository for Trigger operations pub struct TriggerRepository; impl Repository for TriggerRepository { type Entity = Trigger; fn table_name() -> &'static str { "triggers" } } /// Input for creating a new trigger #[derive(Debug, Clone)] pub struct CreateTriggerInput { pub r#ref: String, pub pack: Option, pub pack_ref: Option, pub label: String, pub description: Option, pub enabled: bool, pub param_schema: Option, pub out_schema: Option, pub is_adhoc: bool, } /// Input for updating a trigger #[derive(Debug, Clone, Default)] pub struct UpdateTriggerInput { pub label: Option, pub description: Option>, pub enabled: Option, pub param_schema: Option>, pub out_schema: Option>, } #[async_trait::async_trait] impl FindById for TriggerRepository { async fn find_by_id<'e, E>(executor: E, id: i64) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let trigger = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger WHERE id = $1 "#, ) .bind(id) .fetch_optional(executor) .await?; Ok(trigger) } } #[async_trait::async_trait] impl FindByRef for TriggerRepository { async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let trigger = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger WHERE ref = $1 "#, ) .bind(ref_str) .fetch_optional(executor) .await?; Ok(trigger) } } #[async_trait::async_trait] impl List for TriggerRepository { async fn list<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let triggers = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger ORDER BY ref ASC "#, ) .fetch_all(executor) .await?; Ok(triggers) } } #[async_trait::async_trait] impl Create for TriggerRepository { type CreateInput = CreateTriggerInput; async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let trigger = sqlx::query_as::<_, Trigger>( r#" INSERT INTO trigger (ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, is_adhoc) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated "#, ) .bind(&input.r#ref) .bind(input.pack) .bind(&input.pack_ref) .bind(&input.label) .bind(&input.description) .bind(input.enabled) .bind(&input.param_schema) .bind(&input.out_schema) .bind(input.is_adhoc) .fetch_one(executor) .await .map_err(|e| { // Convert unique constraint violation to AlreadyExists error if let sqlx::Error::Database(db_err) = &e { if db_err.is_unique_violation() { return crate::Error::already_exists("Trigger", "ref", &input.r#ref); } } e.into() })?; Ok(trigger) } } #[async_trait::async_trait] impl Update for TriggerRepository { type UpdateInput = UpdateTriggerInput; async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { // Build update query let mut query = QueryBuilder::new("UPDATE trigger SET "); let mut has_updates = false; if let Some(label) = &input.label { query.push("label = "); query.push_bind(label); has_updates = true; } if let Some(description) = &input.description { if has_updates { query.push(", "); } query.push("description = "); match description { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(enabled) = input.enabled { if has_updates { query.push(", "); } query.push("enabled = "); query.push_bind(enabled); has_updates = true; } if let Some(param_schema) = &input.param_schema { if has_updates { query.push(", "); } query.push("param_schema = "); match param_schema { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(out_schema) = &input.out_schema { if has_updates { query.push(", "); } query.push("out_schema = "); match out_schema { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if !has_updates { // No updates requested, fetch and return existing entity return Self::get_by_id(executor, id).await; } query.push(", updated = NOW() WHERE id = "); query.push_bind(id); query.push(" RETURNING id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated"); let trigger = query .build_query_as::() .fetch_one(executor) .await .map_err(|e| { // Convert RowNotFound to NotFound error if matches!(e, sqlx::Error::RowNotFound) { return crate::Error::not_found("trigger", "id", id.to_string()); } e.into() })?; Ok(trigger) } } #[async_trait::async_trait] impl Delete for TriggerRepository { async fn delete<'e, E>(executor: E, id: i64) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = sqlx::query("DELETE FROM trigger WHERE id = $1") .bind(id) .execute(executor) .await?; Ok(result.rows_affected() > 0) } } impl TriggerRepository { /// Delete non-adhoc triggers belonging to a pack whose refs are NOT in the given set. /// /// Used during pack reinstallation to clean up triggers that were removed /// from the pack's YAML files. Ad-hoc (user-created) triggers are preserved. pub async fn delete_non_adhoc_by_pack_excluding<'e, E>( executor: E, pack_id: Id, keep_refs: &[String], ) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = if keep_refs.is_empty() { sqlx::query("DELETE FROM trigger WHERE pack = $1 AND is_adhoc = false") .bind(pack_id) .execute(executor) .await? } else { sqlx::query( "DELETE FROM trigger WHERE pack = $1 AND is_adhoc = false AND ref != ALL($2)", ) .bind(pack_id) .bind(keep_refs) .execute(executor) .await? }; Ok(result.rows_affected()) } /// Search triggers with all filters pushed into SQL. /// /// All filter fields are combinable (AND). Pagination is server-side. pub async fn list_search<'e, E>( db: E, filters: &TriggerSearchFilters, ) -> Result where E: Executor<'e, Database = Postgres> + Copy + 'e, { let select_cols = "id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated"; let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new(format!("SELECT {select_cols} FROM trigger")); let mut count_qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT COUNT(*) FROM trigger"); let mut has_where = false; macro_rules! push_condition { ($cond_prefix:expr, $value:expr) => {{ if !has_where { qb.push(" WHERE "); count_qb.push(" WHERE "); has_where = true; } else { qb.push(" AND "); count_qb.push(" AND "); } qb.push($cond_prefix); qb.push_bind($value.clone()); count_qb.push($cond_prefix); count_qb.push_bind($value); }}; } if let Some(pack_id) = filters.pack { push_condition!("pack = ", pack_id); } if let Some(enabled) = filters.enabled { push_condition!("enabled = ", enabled); } // Suppress unused-assignment warning from the macro's last expansion. let _ = has_where; // Count let total: i64 = count_qb.build_query_scalar().fetch_one(db).await?; let total = total.max(0) as u64; // Data query qb.push(" ORDER BY ref ASC"); qb.push(" LIMIT "); qb.push_bind(filters.limit as i64); qb.push(" OFFSET "); qb.push_bind(filters.offset as i64); let rows: Vec = qb.build_query_as().fetch_all(db).await?; Ok(TriggerSearchResult { rows, total }) } /// Find triggers by pack ID pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let triggers = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger WHERE pack = $1 ORDER BY ref ASC "#, ) .bind(pack_id) .fetch_all(executor) .await?; Ok(triggers) } /// Find enabled triggers pub async fn find_enabled<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let triggers = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger WHERE enabled = true ORDER BY ref ASC "#, ) .fetch_all(executor) .await?; Ok(triggers) } /// Find trigger by webhook key pub async fn find_by_webhook_key<'e, E>( executor: E, webhook_key: &str, ) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let trigger = sqlx::query_as::<_, Trigger>( r#" SELECT id, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated FROM trigger WHERE webhook_key = $1 "#, ) .bind(webhook_key) .fetch_optional(executor) .await?; Ok(trigger) } /// Enable webhooks for a trigger pub async fn enable_webhook<'e, E>(executor: E, trigger_id: Id) -> Result where E: Executor<'e, Database = Postgres> + 'e, { #[derive(sqlx::FromRow)] struct WebhookResult { webhook_enabled: bool, webhook_key: String, webhook_url: String, } let result = sqlx::query_as::<_, WebhookResult>( r#" SELECT * FROM enable_trigger_webhook($1) "#, ) .bind(trigger_id) .fetch_one(executor) .await?; Ok(WebhookInfo { enabled: result.webhook_enabled, webhook_key: result.webhook_key, webhook_url: result.webhook_url, }) } /// Disable webhooks for a trigger pub async fn disable_webhook<'e, E>(executor: E, trigger_id: Id) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = sqlx::query_scalar::<_, bool>( r#" SELECT disable_trigger_webhook($1) "#, ) .bind(trigger_id) .fetch_one(executor) .await?; Ok(result) } /// Regenerate webhook key for a trigger pub async fn regenerate_webhook_key<'e, E>( executor: E, trigger_id: Id, ) -> Result where E: Executor<'e, Database = Postgres> + 'e, { #[derive(sqlx::FromRow)] struct RegenerateResult { webhook_key: String, previous_key_revoked: bool, } let result = sqlx::query_as::<_, RegenerateResult>( r#" SELECT * FROM regenerate_trigger_webhook_key($1) "#, ) .bind(trigger_id) .fetch_one(executor) .await?; Ok(WebhookKeyRegenerate { webhook_key: result.webhook_key, previous_key_revoked: result.previous_key_revoked, }) } // ======================================================================== // Phase 3: Advanced Webhook Features // ======================================================================== /// Update webhook configuration for a trigger pub async fn update_webhook_config<'e, E>( executor: E, trigger_id: Id, config: serde_json::Value, ) -> Result<()> where E: Executor<'e, Database = Postgres> + 'e, { sqlx::query( r#" UPDATE trigger SET webhook_config = $2, updated = NOW() WHERE id = $1 "#, ) .bind(trigger_id) .bind(config) .execute(executor) .await?; Ok(()) } /// Log webhook event for auditing and analytics pub async fn log_webhook_event<'e, E>(executor: E, input: WebhookEventLogInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let id = sqlx::query_scalar::<_, i64>( r#" INSERT INTO webhook_event_log ( trigger_id, trigger_ref, webhook_key, event_id, source_ip, user_agent, payload_size_bytes, headers, status_code, error_message, processing_time_ms, hmac_verified, rate_limited, ip_allowed ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id "#, ) .bind(input.trigger_id) .bind(input.trigger_ref) .bind(input.webhook_key) .bind(input.event_id) .bind(input.source_ip) .bind(input.user_agent) .bind(input.payload_size_bytes) .bind(input.headers) .bind(input.status_code) .bind(input.error_message) .bind(input.processing_time_ms) .bind(input.hmac_verified) .bind(input.rate_limited) .bind(input.ip_allowed) .fetch_one(executor) .await?; Ok(id) } } /// Webhook information returned when enabling webhooks #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct WebhookInfo { pub enabled: bool, pub webhook_key: String, pub webhook_url: String, } /// Webhook key regeneration result #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct WebhookKeyRegenerate { pub webhook_key: String, pub previous_key_revoked: bool, } /// Input for logging webhook events #[derive(Debug, Clone)] pub struct WebhookEventLogInput { pub trigger_id: Id, pub trigger_ref: String, pub webhook_key: String, pub event_id: Option, pub source_ip: Option, pub user_agent: Option, pub payload_size_bytes: Option, pub headers: Option, pub status_code: i32, pub error_message: Option, pub processing_time_ms: Option, pub hmac_verified: Option, pub rate_limited: bool, pub ip_allowed: Option, } // ============================================================================ // Sensor Repository // ============================================================================ /// Repository for Sensor operations pub struct SensorRepository; impl Repository for SensorRepository { type Entity = Sensor; fn table_name() -> &'static str { "sensor" } } /// Input for creating a new sensor #[derive(Debug, Clone)] pub struct CreateSensorInput { pub r#ref: String, pub pack: Option, pub pack_ref: Option, pub label: String, pub description: Option, pub entrypoint: String, pub runtime: Id, pub runtime_ref: String, pub runtime_version_constraint: Option, pub trigger: Id, pub trigger_ref: String, pub enabled: bool, pub param_schema: Option, pub config: Option, } /// Input for updating a sensor #[derive(Debug, Clone, Default)] pub struct UpdateSensorInput { pub label: Option, pub description: Option>, pub entrypoint: Option, pub runtime: Option, pub runtime_ref: Option, pub runtime_version_constraint: Option>, pub trigger: Option, pub trigger_ref: Option, pub enabled: Option, pub param_schema: Option>, pub config: Option, } #[async_trait::async_trait] impl FindById for SensorRepository { async fn find_by_id<'e, E>(executor: E, id: i64) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensor = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor WHERE id = $1 "#, ) .bind(id) .fetch_optional(executor) .await?; Ok(sensor) } } #[async_trait::async_trait] impl FindByRef for SensorRepository { async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensor = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor WHERE ref = $1 "#, ) .bind(ref_str) .fetch_optional(executor) .await?; Ok(sensor) } } #[async_trait::async_trait] impl List for SensorRepository { async fn list<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensors = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor ORDER BY ref ASC "#, ) .fetch_all(executor) .await?; Ok(sensors) } } #[async_trait::async_trait] impl Create for SensorRepository { type CreateInput = CreateSensorInput; async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let sensor = sqlx::query_as::<_, Sensor>( r#" INSERT INTO sensor (ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated "#, ) .bind(&input.r#ref) .bind(input.pack) .bind(&input.pack_ref) .bind(&input.label) .bind(&input.description) .bind(&input.entrypoint) .bind(input.runtime) .bind(&input.runtime_ref) .bind(&input.runtime_version_constraint) .bind(input.trigger) .bind(&input.trigger_ref) .bind(input.enabled) .bind(&input.param_schema) .bind(&input.config) .fetch_one(executor) .await?; Ok(sensor) } } #[async_trait::async_trait] impl Update for SensorRepository { type UpdateInput = UpdateSensorInput; async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result where E: Executor<'e, Database = Postgres> + 'e, { // Build update query let mut query = QueryBuilder::new("UPDATE sensor SET "); let mut has_updates = false; if let Some(label) = &input.label { query.push("label = "); query.push_bind(label); has_updates = true; } if let Some(description) = &input.description { if has_updates { query.push(", "); } query.push("description = "); match description { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(entrypoint) = &input.entrypoint { if has_updates { query.push(", "); } query.push("entrypoint = "); query.push_bind(entrypoint); has_updates = true; } if let Some(enabled) = input.enabled { if has_updates { query.push(", "); } query.push("enabled = "); query.push_bind(enabled); has_updates = true; } if let Some(runtime) = input.runtime { if has_updates { query.push(", "); } query.push("runtime = "); query.push_bind(runtime); has_updates = true; } if let Some(runtime_ref) = &input.runtime_ref { if has_updates { query.push(", "); } query.push("runtime_ref = "); query.push_bind(runtime_ref); has_updates = true; } if let Some(runtime_version_constraint) = &input.runtime_version_constraint { if has_updates { query.push(", "); } query.push("runtime_version_constraint = "); match runtime_version_constraint { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(trigger) = input.trigger { if has_updates { query.push(", "); } query.push("trigger = "); query.push_bind(trigger); has_updates = true; } if let Some(trigger_ref) = &input.trigger_ref { if has_updates { query.push(", "); } query.push("trigger_ref = "); query.push_bind(trigger_ref); has_updates = true; } if let Some(param_schema) = &input.param_schema { if has_updates { query.push(", "); } query.push("param_schema = "); match param_schema { Patch::Set(value) => query.push_bind(value), Patch::Clear => query.push_bind(Option::::None), }; has_updates = true; } if let Some(config) = &input.config { if has_updates { query.push(", "); } query.push("config = "); query.push_bind(config); has_updates = true; } if !has_updates { // No updates requested, fetch and return existing entity return Self::get_by_id(executor, id).await; } query.push(", updated = NOW() WHERE id = "); query.push_bind(id); query.push(" RETURNING id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated"); let sensor = query.build_query_as::().fetch_one(executor).await?; Ok(sensor) } } #[async_trait::async_trait] impl Delete for SensorRepository { async fn delete<'e, E>(executor: E, id: i64) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = sqlx::query("DELETE FROM sensor WHERE id = $1") .bind(id) .execute(executor) .await?; Ok(result.rows_affected() > 0) } } impl SensorRepository { /// Delete non-adhoc sensors belonging to a pack whose refs are NOT in the given set. /// /// Used during pack reinstallation to clean up sensors that were removed /// from the pack's YAML files. pub async fn delete_by_pack_excluding<'e, E>( executor: E, pack_id: Id, keep_refs: &[String], ) -> Result where E: Executor<'e, Database = Postgres> + 'e, { let result = if keep_refs.is_empty() { sqlx::query("DELETE FROM sensor WHERE pack = $1") .bind(pack_id) .execute(executor) .await? } else { sqlx::query("DELETE FROM sensor WHERE pack = $1 AND ref != ALL($2)") .bind(pack_id) .bind(keep_refs) .execute(executor) .await? }; Ok(result.rows_affected()) } /// Search sensors with all filters pushed into SQL. /// /// All filter fields are combinable (AND). Pagination is server-side. pub async fn list_search<'e, E>( db: E, filters: &SensorSearchFilters, ) -> Result where E: Executor<'e, Database = Postgres> + Copy + 'e, { let select_cols = "id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated"; let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new(format!("SELECT {select_cols} FROM sensor")); let mut count_qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT COUNT(*) FROM sensor"); let mut has_where = false; macro_rules! push_condition { ($cond_prefix:expr, $value:expr) => {{ if !has_where { qb.push(" WHERE "); count_qb.push(" WHERE "); has_where = true; } else { qb.push(" AND "); count_qb.push(" AND "); } qb.push($cond_prefix); qb.push_bind($value.clone()); count_qb.push($cond_prefix); count_qb.push_bind($value); }}; } if let Some(pack_id) = filters.pack { push_condition!("pack = ", pack_id); } if let Some(trigger_id) = filters.trigger { push_condition!("trigger = ", trigger_id); } if let Some(enabled) = filters.enabled { push_condition!("enabled = ", enabled); } // Suppress unused-assignment warning from the macro's last expansion. let _ = has_where; // Count let total: i64 = count_qb.build_query_scalar().fetch_one(db).await?; let total = total.max(0) as u64; // Data query qb.push(" ORDER BY ref ASC"); qb.push(" LIMIT "); qb.push_bind(filters.limit as i64); qb.push(" OFFSET "); qb.push_bind(filters.offset as i64); let rows: Vec = qb.build_query_as().fetch_all(db).await?; Ok(SensorSearchResult { rows, total }) } /// Find sensors by trigger ID pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensors = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor WHERE trigger = $1 ORDER BY ref ASC "#, ) .bind(trigger_id) .fetch_all(executor) .await?; Ok(sensors) } /// Find enabled sensors pub async fn find_enabled<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensors = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor WHERE enabled = true ORDER BY ref ASC "#, ) .fetch_all(executor) .await?; Ok(sensors) } /// Find sensors by pack ID pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result> where E: Executor<'e, Database = Postgres> + 'e, { let sensors = sqlx::query_as::<_, Sensor>( r#" SELECT id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, runtime_version_constraint, trigger, trigger_ref, enabled, param_schema, config, created, updated FROM sensor WHERE pack = $1 ORDER BY ref ASC "#, ) .bind(pack_id) .fetch_all(executor) .await?; Ok(sensors) } }