Some checks failed
CI / Rustfmt (pull_request) Successful in 19s
CI / Cargo Audit & Deny (pull_request) Successful in 33s
CI / Security Blocking Checks (pull_request) Successful in 5s
CI / Web Blocking Checks (pull_request) Successful in 49s
CI / Web Advisory Checks (pull_request) Successful in 33s
CI / Clippy (pull_request) Has been cancelled
CI / Security Advisory Checks (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
803 lines
24 KiB
Rust
803 lines
24 KiB
Rust
//! Event and Enforcement repository for database operations
|
|
//!
|
|
//! This module provides CRUD operations and queries for Event and Enforcement entities.
|
|
//! Note: Events are immutable time-series data — there is no Update impl for EventRepository.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
use crate::models::{
|
|
enums::{EnforcementCondition, EnforcementStatus},
|
|
event::*,
|
|
Id, JsonDict,
|
|
};
|
|
use crate::Result;
|
|
use sqlx::{Executor, Postgres, QueryBuilder};
|
|
|
|
use super::{Create, Delete, FindById, List, Repository, Update};
|
|
|
|
// ============================================================================
|
|
// Event Search
|
|
// ============================================================================
|
|
|
|
/// Filters for [`EventRepository::search`].
|
|
///
|
|
/// All fields are optional. When set, the corresponding WHERE clause is added.
|
|
/// Pagination is always applied.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct EventSearchFilters {
|
|
pub trigger: Option<Id>,
|
|
pub trigger_ref: Option<String>,
|
|
pub source: Option<Id>,
|
|
pub rule_ref: Option<String>,
|
|
pub limit: u32,
|
|
pub offset: u32,
|
|
}
|
|
|
|
/// Result of [`EventRepository::search`].
|
|
#[derive(Debug)]
|
|
pub struct EventSearchResult {
|
|
pub rows: Vec<Event>,
|
|
pub total: u64,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Enforcement Search
|
|
// ============================================================================
|
|
|
|
/// Filters for [`EnforcementRepository::search`].
|
|
///
|
|
/// All fields are optional and combinable. Pagination is always applied.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct EnforcementSearchFilters {
|
|
pub rule: Option<Id>,
|
|
pub event: Option<Id>,
|
|
pub status: Option<EnforcementStatus>,
|
|
pub trigger_ref: Option<String>,
|
|
pub rule_ref: Option<String>,
|
|
pub limit: u32,
|
|
pub offset: u32,
|
|
}
|
|
|
|
/// Result of [`EnforcementRepository::search`].
|
|
#[derive(Debug)]
|
|
pub struct EnforcementSearchResult {
|
|
pub rows: Vec<Enforcement>,
|
|
pub total: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct EnforcementCreateOrGetResult {
|
|
pub enforcement: Enforcement,
|
|
pub created: bool,
|
|
}
|
|
|
|
/// Repository for Event operations
|
|
pub struct EventRepository;
|
|
|
|
impl Repository for EventRepository {
|
|
type Entity = Event;
|
|
|
|
fn table_name() -> &'static str {
|
|
"event"
|
|
}
|
|
}
|
|
|
|
/// Input for creating a new event
|
|
#[derive(Debug, Clone)]
|
|
pub struct CreateEventInput {
|
|
pub trigger: Option<Id>,
|
|
pub trigger_ref: String,
|
|
pub config: Option<JsonDict>,
|
|
pub payload: Option<JsonDict>,
|
|
pub source: Option<Id>,
|
|
pub source_ref: Option<String>,
|
|
pub rule: Option<Id>,
|
|
pub rule_ref: Option<String>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindById for EventRepository {
|
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let event = sqlx::query_as::<_, Event>(
|
|
r#"
|
|
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
|
rule, rule_ref, created
|
|
FROM event
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(event)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl List for EventRepository {
|
|
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let events = sqlx::query_as::<_, Event>(
|
|
r#"
|
|
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
|
rule, rule_ref, created
|
|
FROM event
|
|
ORDER BY created DESC
|
|
LIMIT 1000
|
|
"#,
|
|
)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(events)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Create for EventRepository {
|
|
type CreateInput = CreateEventInput;
|
|
|
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let event = sqlx::query_as::<_, Event>(
|
|
r#"
|
|
INSERT INTO event (trigger, trigger_ref, config, payload, source, source_ref, rule, rule_ref)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id, trigger, trigger_ref, config, payload, source, source_ref,
|
|
rule, rule_ref, created
|
|
"#,
|
|
)
|
|
.bind(input.trigger)
|
|
.bind(&input.trigger_ref)
|
|
.bind(&input.config)
|
|
.bind(&input.payload)
|
|
.bind(input.source)
|
|
.bind(&input.source_ref)
|
|
.bind(input.rule)
|
|
.bind(&input.rule_ref)
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(event)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Delete for EventRepository {
|
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query("DELETE FROM event WHERE id = $1")
|
|
.bind(id)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected() > 0)
|
|
}
|
|
}
|
|
|
|
impl EventRepository {
|
|
/// Find events by trigger ID
|
|
pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result<Vec<Event>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let events = sqlx::query_as::<_, Event>(
|
|
r#"
|
|
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
|
rule, rule_ref, created
|
|
FROM event
|
|
WHERE trigger = $1
|
|
ORDER BY created DESC
|
|
LIMIT 1000
|
|
"#,
|
|
)
|
|
.bind(trigger_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(events)
|
|
}
|
|
|
|
/// Find events by trigger ref
|
|
pub async fn find_by_trigger_ref<'e, E>(executor: E, trigger_ref: &str) -> Result<Vec<Event>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let events = sqlx::query_as::<_, Event>(
|
|
r#"
|
|
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
|
rule, rule_ref, created
|
|
FROM event
|
|
WHERE trigger_ref = $1
|
|
ORDER BY created DESC
|
|
LIMIT 1000
|
|
"#,
|
|
)
|
|
.bind(trigger_ref)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(events)
|
|
}
|
|
|
|
/// Search events with all filters pushed into SQL.
|
|
///
|
|
/// Builds a dynamic query so that every filter, pagination, and the total
|
|
/// count are handled in the database — no in-memory filtering or slicing.
|
|
pub async fn search<'e, E>(db: E, filters: &EventSearchFilters) -> Result<EventSearchResult>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
|
{
|
|
let select_cols = "id, trigger, trigger_ref, config, payload, source, source_ref, rule, rule_ref, created";
|
|
|
|
let mut qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new(format!("SELECT {select_cols} FROM event"));
|
|
let mut count_qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new("SELECT COUNT(*) FROM event");
|
|
|
|
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(trigger_id) = filters.trigger {
|
|
push_condition!("trigger = ", trigger_id);
|
|
}
|
|
if let Some(ref trigger_ref) = filters.trigger_ref {
|
|
push_condition!("trigger_ref = ", trigger_ref.clone());
|
|
}
|
|
if let Some(source_id) = filters.source {
|
|
push_condition!("source = ", source_id);
|
|
}
|
|
if let Some(ref rule_ref) = filters.rule_ref {
|
|
push_condition!(
|
|
"LOWER(rule_ref) LIKE ",
|
|
format!("%{}%", rule_ref.to_lowercase())
|
|
);
|
|
}
|
|
|
|
// 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 created DESC");
|
|
qb.push(" LIMIT ");
|
|
qb.push_bind(filters.limit as i64);
|
|
qb.push(" OFFSET ");
|
|
qb.push_bind(filters.offset as i64);
|
|
|
|
let rows: Vec<Event> = qb.build_query_as().fetch_all(db).await?;
|
|
|
|
Ok(EventSearchResult { rows, total })
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Enforcement Repository
|
|
// ============================================================================
|
|
|
|
/// Repository for Enforcement operations
|
|
pub struct EnforcementRepository;
|
|
|
|
impl Repository for EnforcementRepository {
|
|
type Entity = Enforcement;
|
|
|
|
fn table_name() -> &'static str {
|
|
"enforcement"
|
|
}
|
|
}
|
|
|
|
/// Input for creating a new enforcement
|
|
#[derive(Debug, Clone)]
|
|
pub struct CreateEnforcementInput {
|
|
pub rule: Option<Id>,
|
|
pub rule_ref: String,
|
|
pub trigger_ref: String,
|
|
pub config: Option<JsonDict>,
|
|
pub event: Option<Id>,
|
|
pub status: EnforcementStatus,
|
|
pub payload: JsonDict,
|
|
pub condition: EnforcementCondition,
|
|
pub conditions: serde_json::Value,
|
|
}
|
|
|
|
/// Input for updating an enforcement
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct UpdateEnforcementInput {
|
|
pub status: Option<EnforcementStatus>,
|
|
pub payload: Option<JsonDict>,
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindById for EnforcementRepository {
|
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcement = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(enforcement)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl List for EnforcementRepository {
|
|
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcements = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
ORDER BY created DESC
|
|
LIMIT 1000
|
|
"#,
|
|
)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(enforcements)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Create for EnforcementRepository {
|
|
type CreateInput = CreateEnforcementInput;
|
|
|
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcement = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
INSERT INTO enforcement (rule, rule_ref, trigger_ref, config, event, status,
|
|
payload, condition, conditions)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
"#,
|
|
)
|
|
.bind(input.rule)
|
|
.bind(&input.rule_ref)
|
|
.bind(&input.trigger_ref)
|
|
.bind(&input.config)
|
|
.bind(input.event)
|
|
.bind(input.status)
|
|
.bind(&input.payload)
|
|
.bind(input.condition)
|
|
.bind(&input.conditions)
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(enforcement)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Update for EnforcementRepository {
|
|
type UpdateInput = UpdateEnforcementInput;
|
|
|
|
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
if input.status.is_none() && input.payload.is_none() && input.resolved_at.is_none() {
|
|
return Self::get_by_id(executor, id).await;
|
|
}
|
|
|
|
Self::update_with_locator(executor, input, |query| {
|
|
query.push(" WHERE id = ");
|
|
query.push_bind(id);
|
|
})
|
|
.await
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Delete for EnforcementRepository {
|
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query("DELETE FROM enforcement WHERE id = $1")
|
|
.bind(id)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected() > 0)
|
|
}
|
|
}
|
|
|
|
impl EnforcementRepository {
|
|
async fn update_with_locator<'e, E, F>(
|
|
executor: E,
|
|
input: UpdateEnforcementInput,
|
|
where_clause: F,
|
|
) -> Result<Enforcement>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
F: FnOnce(&mut QueryBuilder<'_, Postgres>),
|
|
{
|
|
let mut query = QueryBuilder::new("UPDATE enforcement SET ");
|
|
let mut has_updates = false;
|
|
|
|
if let Some(status) = input.status {
|
|
query.push("status = ");
|
|
query.push_bind(status);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(payload) = &input.payload {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("payload = ");
|
|
query.push_bind(payload);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(resolved_at) = input.resolved_at {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("resolved_at = ");
|
|
query.push_bind(resolved_at);
|
|
}
|
|
|
|
where_clause(&mut query);
|
|
query.push(
|
|
" RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload, \
|
|
condition, conditions, created, resolved_at",
|
|
);
|
|
|
|
let enforcement = query
|
|
.build_query_as::<Enforcement>()
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(enforcement)
|
|
}
|
|
|
|
/// Update an enforcement using the loaded row's primary key.
|
|
pub async fn update_loaded<'e, E>(
|
|
executor: E,
|
|
enforcement: &Enforcement,
|
|
input: UpdateEnforcementInput,
|
|
) -> Result<Enforcement>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
if input.status.is_none() && input.payload.is_none() && input.resolved_at.is_none() {
|
|
return Ok(enforcement.clone());
|
|
}
|
|
|
|
Self::update_with_locator(executor, input, |query| {
|
|
query.push(" WHERE id = ");
|
|
query.push_bind(enforcement.id);
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub async fn update_loaded_if_status<'e, E>(
|
|
executor: E,
|
|
enforcement: &Enforcement,
|
|
expected_status: EnforcementStatus,
|
|
input: UpdateEnforcementInput,
|
|
) -> Result<Option<Enforcement>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
if input.status.is_none() && input.payload.is_none() && input.resolved_at.is_none() {
|
|
return Ok(Some(enforcement.clone()));
|
|
}
|
|
|
|
let mut query = QueryBuilder::new("UPDATE enforcement SET ");
|
|
let mut has_updates = false;
|
|
|
|
if let Some(status) = input.status {
|
|
query.push("status = ");
|
|
query.push_bind(status);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(payload) = &input.payload {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("payload = ");
|
|
query.push_bind(payload);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(resolved_at) = input.resolved_at {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("resolved_at = ");
|
|
query.push_bind(resolved_at);
|
|
has_updates = true;
|
|
}
|
|
|
|
if !has_updates {
|
|
return Ok(Some(enforcement.clone()));
|
|
}
|
|
|
|
query.push(" WHERE id = ");
|
|
query.push_bind(enforcement.id);
|
|
query.push(" AND status = ");
|
|
query.push_bind(expected_status);
|
|
query.push(
|
|
" RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload, \
|
|
condition, conditions, created, resolved_at",
|
|
);
|
|
|
|
query
|
|
.build_query_as::<Enforcement>()
|
|
.fetch_optional(executor)
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
/// Find enforcements by rule ID
|
|
pub async fn find_by_rule<'e, E>(executor: E, rule_id: Id) -> Result<Vec<Enforcement>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcements = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
WHERE rule = $1
|
|
ORDER BY created DESC
|
|
"#,
|
|
)
|
|
.bind(rule_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(enforcements)
|
|
}
|
|
|
|
/// Find enforcements by status
|
|
pub async fn find_by_status<'e, E>(
|
|
executor: E,
|
|
status: EnforcementStatus,
|
|
) -> Result<Vec<Enforcement>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcements = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
WHERE status = $1
|
|
ORDER BY created DESC
|
|
"#,
|
|
)
|
|
.bind(status)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(enforcements)
|
|
}
|
|
|
|
/// Find enforcements by event ID
|
|
pub async fn find_by_event<'e, E>(executor: E, event_id: Id) -> Result<Vec<Enforcement>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let enforcements = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
WHERE event = $1
|
|
ORDER BY created DESC
|
|
"#,
|
|
)
|
|
.bind(event_id)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(enforcements)
|
|
}
|
|
|
|
pub async fn find_by_rule_and_event<'e, E>(
|
|
executor: E,
|
|
rule_id: Id,
|
|
event_id: Id,
|
|
) -> Result<Option<Enforcement>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
FROM enforcement
|
|
WHERE rule = $1 AND event = $2
|
|
LIMIT 1
|
|
"#,
|
|
)
|
|
.bind(rule_id)
|
|
.bind(event_id)
|
|
.fetch_optional(executor)
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
pub async fn create_or_get_by_rule_event<'e, E>(
|
|
executor: E,
|
|
input: CreateEnforcementInput,
|
|
) -> Result<EnforcementCreateOrGetResult>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
|
{
|
|
let (Some(rule_id), Some(event_id)) = (input.rule, input.event) else {
|
|
let enforcement = Self::create(executor, input).await?;
|
|
return Ok(EnforcementCreateOrGetResult {
|
|
enforcement,
|
|
created: true,
|
|
});
|
|
};
|
|
|
|
let inserted = sqlx::query_as::<_, Enforcement>(
|
|
r#"
|
|
INSERT INTO enforcement (rule, rule_ref, trigger_ref, config, event, status,
|
|
payload, condition, conditions)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
ON CONFLICT (rule, event) WHERE rule IS NOT NULL AND event IS NOT NULL DO NOTHING
|
|
RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
|
condition, conditions, created, resolved_at
|
|
"#,
|
|
)
|
|
.bind(input.rule)
|
|
.bind(&input.rule_ref)
|
|
.bind(&input.trigger_ref)
|
|
.bind(&input.config)
|
|
.bind(input.event)
|
|
.bind(input.status)
|
|
.bind(&input.payload)
|
|
.bind(input.condition)
|
|
.bind(&input.conditions)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
if let Some(enforcement) = inserted {
|
|
return Ok(EnforcementCreateOrGetResult {
|
|
enforcement,
|
|
created: true,
|
|
});
|
|
}
|
|
|
|
let enforcement = Self::find_by_rule_and_event(executor, rule_id, event_id)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
anyhow::anyhow!(
|
|
"enforcement for rule {} and event {} disappeared after dedupe conflict",
|
|
rule_id,
|
|
event_id
|
|
)
|
|
})?;
|
|
|
|
Ok(EnforcementCreateOrGetResult {
|
|
enforcement,
|
|
created: false,
|
|
})
|
|
}
|
|
|
|
/// Search enforcements with all filters pushed into SQL.
|
|
///
|
|
/// All filter fields are combinable (AND). Pagination is server-side.
|
|
pub async fn search<'e, E>(
|
|
db: E,
|
|
filters: &EnforcementSearchFilters,
|
|
) -> Result<EnforcementSearchResult>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + Copy + 'e,
|
|
{
|
|
let select_cols = "id, rule, rule_ref, trigger_ref, config, event, status, payload, condition, conditions, created, resolved_at";
|
|
|
|
let mut qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new(format!("SELECT {select_cols} FROM enforcement"));
|
|
let mut count_qb: QueryBuilder<'_, Postgres> =
|
|
QueryBuilder::new("SELECT COUNT(*) FROM enforcement");
|
|
|
|
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(status) = &filters.status {
|
|
push_condition!("status = ", *status);
|
|
}
|
|
if let Some(rule_id) = filters.rule {
|
|
push_condition!("rule = ", rule_id);
|
|
}
|
|
if let Some(event_id) = filters.event {
|
|
push_condition!("event = ", event_id);
|
|
}
|
|
if let Some(ref trigger_ref) = filters.trigger_ref {
|
|
push_condition!("trigger_ref = ", trigger_ref.clone());
|
|
}
|
|
if let Some(ref rule_ref) = filters.rule_ref {
|
|
push_condition!("rule_ref = ", rule_ref.clone());
|
|
}
|
|
|
|
// 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 created DESC");
|
|
qb.push(" LIMIT ");
|
|
qb.push_bind(filters.limit as i64);
|
|
qb.push(" OFFSET ");
|
|
qb.push_bind(filters.offset as i64);
|
|
|
|
let rows: Vec<Enforcement> = qb.build_query_as().fetch_all(db).await?;
|
|
|
|
Ok(EnforcementSearchResult { rows, total })
|
|
}
|
|
}
|