570 lines
18 KiB
Rust
570 lines
18 KiB
Rust
//! Rule repository for database operations
|
|
//!
|
|
//! This module provides CRUD operations and queries for Rule entities.
|
|
|
|
use crate::models::{rule::*, Id};
|
|
use crate::{Error, Result};
|
|
use sqlx::{Executor, Postgres, QueryBuilder};
|
|
|
|
use super::{Create, Delete, FindById, FindByRef, List, Patch, Repository, Update};
|
|
|
|
/// Filters for [`RuleRepository::list_search`].
|
|
///
|
|
/// All fields are optional and combinable (AND). Pagination is always applied.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RuleSearchFilters {
|
|
/// Filter by pack ID
|
|
pub pack: Option<Id>,
|
|
/// Filter by action ID
|
|
pub action: Option<Id>,
|
|
/// Filter by trigger ID
|
|
pub trigger: Option<Id>,
|
|
/// Filter by enabled status
|
|
pub enabled: Option<bool>,
|
|
pub limit: u32,
|
|
pub offset: u32,
|
|
}
|
|
|
|
/// Result of [`RuleRepository::list_search`].
|
|
#[derive(Debug)]
|
|
pub struct RuleSearchResult {
|
|
pub rows: Vec<Rule>,
|
|
pub total: u64,
|
|
}
|
|
|
|
/// Input for restoring an ad-hoc rule during pack reinstallation.
|
|
/// Unlike `CreateRuleInput`, action and trigger IDs are optional because
|
|
/// the referenced entities may not exist yet or may have been removed.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RestoreRuleInput {
|
|
pub r#ref: String,
|
|
pub pack: Id,
|
|
pub pack_ref: String,
|
|
pub label: String,
|
|
pub description: Option<String>,
|
|
pub action: Option<Id>,
|
|
pub action_ref: String,
|
|
pub trigger: Option<Id>,
|
|
pub trigger_ref: String,
|
|
pub conditions: serde_json::Value,
|
|
pub action_params: serde_json::Value,
|
|
pub trigger_params: serde_json::Value,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
/// Repository for Rule operations
|
|
pub struct RuleRepository;
|
|
|
|
impl Repository for RuleRepository {
|
|
type Entity = Rule;
|
|
|
|
fn table_name() -> &'static str {
|
|
"rules"
|
|
}
|
|
}
|
|
|
|
/// Input for creating a new rule
|
|
#[derive(Debug, Clone)]
|
|
pub struct CreateRuleInput {
|
|
pub r#ref: String,
|
|
pub pack: Id,
|
|
pub pack_ref: String,
|
|
pub label: String,
|
|
pub description: Option<String>,
|
|
pub action: Id,
|
|
pub action_ref: String,
|
|
pub trigger: Id,
|
|
pub trigger_ref: String,
|
|
pub conditions: serde_json::Value,
|
|
pub action_params: serde_json::Value,
|
|
pub trigger_params: serde_json::Value,
|
|
pub enabled: bool,
|
|
pub is_adhoc: bool,
|
|
}
|
|
|
|
/// Input for updating a rule
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct UpdateRuleInput {
|
|
pub label: Option<String>,
|
|
pub description: Option<Patch<String>>,
|
|
pub conditions: Option<serde_json::Value>,
|
|
pub action_params: Option<serde_json::Value>,
|
|
pub trigger_params: Option<serde_json::Value>,
|
|
pub enabled: Option<bool>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindById for RuleRepository {
|
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rule = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(rule)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindByRef for RuleRepository {
|
|
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rule = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE ref = $1
|
|
"#,
|
|
)
|
|
.bind(ref_str)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(rule)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl List for RuleRepository {
|
|
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Create for RuleRepository {
|
|
type CreateInput = CreateRuleInput;
|
|
|
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rule = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
INSERT INTO rule (ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
RETURNING id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
"#,
|
|
)
|
|
.bind(&input.r#ref)
|
|
.bind(input.pack)
|
|
.bind(&input.pack_ref)
|
|
.bind(&input.label)
|
|
.bind(&input.description)
|
|
.bind(input.action)
|
|
.bind(&input.action_ref)
|
|
.bind(input.trigger)
|
|
.bind(&input.trigger_ref)
|
|
.bind(&input.conditions)
|
|
.bind(&input.action_params)
|
|
.bind(&input.trigger_params)
|
|
.bind(input.enabled)
|
|
.bind(input.is_adhoc)
|
|
.fetch_one(executor)
|
|
.await
|
|
.map_err(|e| {
|
|
if let sqlx::Error::Database(ref db_err) = e {
|
|
if db_err.is_unique_violation() {
|
|
return Error::already_exists("Rule", "ref", &input.r#ref);
|
|
}
|
|
}
|
|
e.into()
|
|
})?;
|
|
|
|
Ok(rule)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Update for RuleRepository {
|
|
type UpdateInput = UpdateRuleInput;
|
|
|
|
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
// Build update query
|
|
|
|
let mut query = QueryBuilder::new("UPDATE rule 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::<String>::None),
|
|
};
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(conditions) = &input.conditions {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("conditions = ");
|
|
query.push_bind(conditions);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(action_params) = &input.action_params {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("action_params = ");
|
|
query.push_bind(action_params);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(trigger_params) = &input.trigger_params {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("trigger_params = ");
|
|
query.push_bind(trigger_params);
|
|
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 !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, action, action_ref, trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated");
|
|
|
|
let rule = query.build_query_as::<Rule>().fetch_one(executor).await?;
|
|
|
|
Ok(rule)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Delete for RuleRepository {
|
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query("DELETE FROM rule WHERE id = $1")
|
|
.bind(id)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected() > 0)
|
|
}
|
|
}
|
|
|
|
impl RuleRepository {
|
|
/// Search rules 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: &RuleSearchFilters) -> Result<RuleSearchResult>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
|
{
|
|
let select_cols = "id, ref, pack, pack_ref, label, description, action, action_ref, trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated";
|
|
|
|
let mut qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new(format!("SELECT {select_cols} FROM rule"));
|
|
let mut count_qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new("SELECT COUNT(*) FROM rule");
|
|
|
|
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(action_id) = filters.action {
|
|
push_condition!("action = ", action_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<Rule> = qb.build_query_as().fetch_all(db).await?;
|
|
|
|
Ok(RuleSearchResult { rows, total })
|
|
}
|
|
|
|
/// Find rules by pack ID
|
|
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Rule>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE pack = $1
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.bind(pack_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Find rules by action ID
|
|
pub async fn find_by_action<'e, E>(executor: E, action_id: Id) -> Result<Vec<Rule>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE action = $1
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.bind(action_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Find rules by trigger ID
|
|
pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result<Vec<Rule>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE trigger = $1
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.bind(trigger_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Find enabled rules
|
|
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<Rule>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE enabled = true
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Find ad-hoc (user-created) rules belonging to a specific pack.
|
|
/// Used to preserve custom rules during pack reinstallation.
|
|
pub async fn find_adhoc_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Rule>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rules = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
FROM rule
|
|
WHERE pack = $1 AND is_adhoc = true
|
|
ORDER BY ref ASC
|
|
"#,
|
|
)
|
|
.bind(pack_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Restore an ad-hoc rule after pack reinstallation.
|
|
/// Accepts `Option<Id>` for action and trigger so the rule is preserved
|
|
/// even if its referenced entities no longer exist.
|
|
pub async fn restore_rule<'e, E>(executor: E, input: RestoreRuleInput) -> Result<Rule>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let rule = sqlx::query_as::<_, Rule>(
|
|
r#"
|
|
INSERT INTO rule (ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, true)
|
|
RETURNING id, ref, pack, pack_ref, label, description, action, action_ref,
|
|
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
|
"#,
|
|
)
|
|
.bind(&input.r#ref)
|
|
.bind(input.pack)
|
|
.bind(&input.pack_ref)
|
|
.bind(&input.label)
|
|
.bind(&input.description)
|
|
.bind(input.action)
|
|
.bind(&input.action_ref)
|
|
.bind(input.trigger)
|
|
.bind(&input.trigger_ref)
|
|
.bind(&input.conditions)
|
|
.bind(&input.action_params)
|
|
.bind(&input.trigger_params)
|
|
.bind(input.enabled)
|
|
.fetch_one(executor)
|
|
.await
|
|
.map_err(|e| {
|
|
if let sqlx::Error::Database(ref db_err) = e {
|
|
if db_err.is_unique_violation() {
|
|
return Error::already_exists("Rule", "ref", &input.r#ref);
|
|
}
|
|
}
|
|
e.into()
|
|
})?;
|
|
|
|
Ok(rule)
|
|
}
|
|
|
|
/// Re-link rules whose action FK is NULL back to a newly recreated action,
|
|
/// matched by `action_ref`. Used after pack reinstallation to fix rules
|
|
/// from other packs that referenced actions in the reinstalled pack.
|
|
pub async fn relink_action_by_ref<'e, E>(
|
|
executor: E,
|
|
action_ref: &str,
|
|
action_id: Id,
|
|
) -> Result<u64>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE rule
|
|
SET action = $1, updated = NOW()
|
|
WHERE action IS NULL AND action_ref = $2
|
|
"#,
|
|
)
|
|
.bind(action_id)
|
|
.bind(action_ref)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected())
|
|
}
|
|
|
|
/// Re-link rules whose trigger FK is NULL back to a newly recreated trigger,
|
|
/// matched by `trigger_ref`. Used after pack reinstallation to fix rules
|
|
/// from other packs that referenced triggers in the reinstalled pack.
|
|
pub async fn relink_trigger_by_ref<'e, E>(
|
|
executor: E,
|
|
trigger_ref: &str,
|
|
trigger_id: Id,
|
|
) -> Result<u64>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query(
|
|
r#"
|
|
UPDATE rule
|
|
SET trigger = $1, updated = NOW()
|
|
WHERE trigger IS NULL AND trigger_ref = $2
|
|
"#,
|
|
)
|
|
.bind(trigger_id)
|
|
.bind(trigger_ref)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected())
|
|
}
|
|
}
|