[WIP] Workflows
This commit is contained in:
@@ -896,7 +896,6 @@ pub mod action {
|
||||
pub runtime_version_constraint: Option<String>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
pub is_workflow: bool,
|
||||
pub workflow_def: Option<Id>,
|
||||
pub is_adhoc: bool,
|
||||
#[sqlx(default)]
|
||||
@@ -988,7 +987,6 @@ pub mod event {
|
||||
pub source: Option<Id>,
|
||||
pub source_ref: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub rule: Option<Id>,
|
||||
pub rule_ref: Option<String>,
|
||||
}
|
||||
@@ -1006,7 +1004,7 @@ pub mod event {
|
||||
pub condition: EnforcementCondition,
|
||||
pub conditions: JsonValue,
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1484,8 +1482,6 @@ pub mod entity_history {
|
||||
pub enum HistoryEntityType {
|
||||
Execution,
|
||||
Worker,
|
||||
Enforcement,
|
||||
Event,
|
||||
}
|
||||
|
||||
impl HistoryEntityType {
|
||||
@@ -1494,8 +1490,6 @@ pub mod entity_history {
|
||||
match self {
|
||||
Self::Execution => "execution_history",
|
||||
Self::Worker => "worker_history",
|
||||
Self::Enforcement => "enforcement_history",
|
||||
Self::Event => "event_history",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1505,8 +1499,6 @@ pub mod entity_history {
|
||||
match self {
|
||||
Self::Execution => write!(f, "execution"),
|
||||
Self::Worker => write!(f, "worker"),
|
||||
Self::Enforcement => write!(f, "enforcement"),
|
||||
Self::Event => write!(f, "event"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1518,10 +1510,8 @@ pub mod entity_history {
|
||||
match s.to_lowercase().as_str() {
|
||||
"execution" => Ok(Self::Execution),
|
||||
"worker" => Ok(Self::Worker),
|
||||
"enforcement" => Ok(Self::Enforcement),
|
||||
"event" => Ok(Self::Event),
|
||||
other => Err(format!(
|
||||
"unknown history entity type '{}'; expected one of: execution, worker, enforcement, event",
|
||||
"unknown history entity type '{}'; expected one of: execution, worker",
|
||||
other
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ impl FindById for ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE id = $1
|
||||
"#,
|
||||
@@ -80,7 +80,7 @@ impl FindByRef for ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
@@ -103,7 +103,7 @@ impl List for ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
@@ -142,7 +142,7 @@ impl Create for ActionRepository {
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
@@ -256,7 +256,7 @@ impl Update for ActionRepository {
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ");
|
||||
query.push_bind(id);
|
||||
query.push(" RETURNING id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_version_constraint, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated");
|
||||
query.push(" RETURNING id, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_version_constraint, param_schema, out_schema, workflow_def, is_adhoc, created, updated");
|
||||
|
||||
let action = query
|
||||
.build_query_as::<Action>()
|
||||
@@ -296,7 +296,7 @@ impl ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
@@ -318,7 +318,7 @@ impl ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE runtime = $1
|
||||
ORDER BY ref ASC
|
||||
@@ -341,7 +341,7 @@ impl ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE LOWER(ref) LIKE $1 OR LOWER(label) LIKE $1 OR LOWER(description) LIKE $1
|
||||
ORDER BY ref ASC
|
||||
@@ -354,7 +354,7 @@ impl ActionRepository {
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
/// Find all workflow actions (actions where is_workflow = true)
|
||||
/// Find all workflow actions (actions linked to a workflow definition)
|
||||
pub async fn find_workflows<'e, E>(executor: E) -> Result<Vec<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
@@ -363,9 +363,9 @@ impl ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE is_workflow = true
|
||||
WHERE workflow_def IS NOT NULL
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
@@ -387,7 +387,7 @@ impl ActionRepository {
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE workflow_def = $1
|
||||
"#,
|
||||
@@ -411,11 +411,11 @@ impl ActionRepository {
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
UPDATE action
|
||||
SET is_workflow = true, workflow_def = $2, updated = NOW()
|
||||
SET workflow_def = $2, updated = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_version_constraint,
|
||||
param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
param_schema, out_schema, workflow_def, is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
|
||||
@@ -80,6 +80,19 @@ pub struct EnforcementVolumeBucket {
|
||||
pub enforcement_count: i64,
|
||||
}
|
||||
|
||||
/// A single hourly bucket of execution volume (from execution hypertable directly).
|
||||
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||
pub struct ExecutionVolumeBucket {
|
||||
/// Start of the 1-hour bucket
|
||||
pub bucket: DateTime<Utc>,
|
||||
/// Action ref; NULL when grouped across all actions
|
||||
pub action_ref: Option<String>,
|
||||
/// The initial status at creation time
|
||||
pub initial_status: Option<String>,
|
||||
/// Number of executions created in this bucket
|
||||
pub execution_count: i64,
|
||||
}
|
||||
|
||||
/// Aggregated failure rate over a time range.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FailureRateSummary {
|
||||
@@ -454,6 +467,69 @@ impl AnalyticsRepository {
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Execution volume (from execution hypertable directly)
|
||||
// =======================================================================
|
||||
|
||||
/// Query the `execution_volume_hourly` continuous aggregate for execution
|
||||
/// creation volume across all actions.
|
||||
pub async fn execution_volume_hourly<'e, E>(
|
||||
executor: E,
|
||||
range: &AnalyticsTimeRange,
|
||||
) -> Result<Vec<ExecutionVolumeBucket>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, ExecutionVolumeBucket>(
|
||||
r#"
|
||||
SELECT
|
||||
bucket,
|
||||
NULL::text AS action_ref,
|
||||
initial_status::text AS initial_status,
|
||||
SUM(execution_count)::bigint AS execution_count
|
||||
FROM execution_volume_hourly
|
||||
WHERE bucket >= $1 AND bucket <= $2
|
||||
GROUP BY bucket, initial_status
|
||||
ORDER BY bucket ASC, initial_status
|
||||
"#,
|
||||
)
|
||||
.bind(range.since)
|
||||
.bind(range.until)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Query the `execution_volume_hourly` continuous aggregate filtered by
|
||||
/// a specific action ref.
|
||||
pub async fn execution_volume_hourly_by_action<'e, E>(
|
||||
executor: E,
|
||||
range: &AnalyticsTimeRange,
|
||||
action_ref: &str,
|
||||
) -> Result<Vec<ExecutionVolumeBucket>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, ExecutionVolumeBucket>(
|
||||
r#"
|
||||
SELECT
|
||||
bucket,
|
||||
action_ref,
|
||||
initial_status::text AS initial_status,
|
||||
execution_count
|
||||
FROM execution_volume_hourly
|
||||
WHERE bucket >= $1 AND bucket <= $2 AND action_ref = $3
|
||||
ORDER BY bucket ASC, initial_status
|
||||
"#,
|
||||
)
|
||||
.bind(range.since)
|
||||
.bind(range.until)
|
||||
.bind(action_ref)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Derived analytics
|
||||
// =======================================================================
|
||||
|
||||
@@ -263,11 +263,6 @@ mod tests {
|
||||
"execution_history"
|
||||
);
|
||||
assert_eq!(HistoryEntityType::Worker.table_name(), "worker_history");
|
||||
assert_eq!(
|
||||
HistoryEntityType::Enforcement.table_name(),
|
||||
"enforcement_history"
|
||||
);
|
||||
assert_eq!(HistoryEntityType::Event.table_name(), "event_history");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -280,14 +275,8 @@ mod tests {
|
||||
"Worker".parse::<HistoryEntityType>().unwrap(),
|
||||
HistoryEntityType::Worker
|
||||
);
|
||||
assert_eq!(
|
||||
"ENFORCEMENT".parse::<HistoryEntityType>().unwrap(),
|
||||
HistoryEntityType::Enforcement
|
||||
);
|
||||
assert_eq!(
|
||||
"event".parse::<HistoryEntityType>().unwrap(),
|
||||
HistoryEntityType::Event
|
||||
);
|
||||
assert!("enforcement".parse::<HistoryEntityType>().is_err());
|
||||
assert!("event".parse::<HistoryEntityType>().is_err());
|
||||
assert!("unknown".parse::<HistoryEntityType>().is_err());
|
||||
}
|
||||
|
||||
@@ -295,7 +284,5 @@ mod tests {
|
||||
fn test_history_entity_type_display() {
|
||||
assert_eq!(HistoryEntityType::Execution.to_string(), "execution");
|
||||
assert_eq!(HistoryEntityType::Worker.to_string(), "worker");
|
||||
assert_eq!(HistoryEntityType::Enforcement.to_string(), "enforcement");
|
||||
assert_eq!(HistoryEntityType::Event.to_string(), "event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! 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},
|
||||
@@ -36,13 +39,6 @@ pub struct CreateEventInput {
|
||||
pub rule_ref: Option<String>,
|
||||
}
|
||||
|
||||
/// Input for updating an event
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateEventInput {
|
||||
pub config: Option<JsonDict>,
|
||||
pub payload: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for EventRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
@@ -52,7 +48,7 @@ impl FindById for EventRepository {
|
||||
let event = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
rule, rule_ref, created
|
||||
FROM event
|
||||
WHERE id = $1
|
||||
"#,
|
||||
@@ -74,7 +70,7 @@ impl List for EventRepository {
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
rule, rule_ref, created
|
||||
FROM event
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
@@ -100,7 +96,7 @@ impl Create for EventRepository {
|
||||
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, updated
|
||||
rule, rule_ref, created
|
||||
"#,
|
||||
)
|
||||
.bind(input.trigger)
|
||||
@@ -118,49 +114,6 @@ impl Create for EventRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for EventRepository {
|
||||
type UpdateInput = UpdateEventInput;
|
||||
|
||||
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 event SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(config) = &input.config {
|
||||
query.push("config = ");
|
||||
query.push_bind(config);
|
||||
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 !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, trigger, trigger_ref, config, payload, source, source_ref, rule, rule_ref, created, updated");
|
||||
|
||||
let event = query.build_query_as::<Event>().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>
|
||||
@@ -185,7 +138,7 @@ impl EventRepository {
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
rule, rule_ref, created
|
||||
FROM event
|
||||
WHERE trigger = $1
|
||||
ORDER BY created DESC
|
||||
@@ -207,7 +160,7 @@ impl EventRepository {
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
rule, rule_ref, created
|
||||
FROM event
|
||||
WHERE trigger_ref = $1
|
||||
ORDER BY created DESC
|
||||
@@ -256,6 +209,7 @@ pub struct CreateEnforcementInput {
|
||||
pub struct UpdateEnforcementInput {
|
||||
pub status: Option<EnforcementStatus>,
|
||||
pub payload: Option<JsonDict>,
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -267,7 +221,7 @@ impl FindById for EnforcementRepository {
|
||||
let enforcement = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
condition, conditions, created, resolved_at
|
||||
FROM enforcement
|
||||
WHERE id = $1
|
||||
"#,
|
||||
@@ -289,7 +243,7 @@ impl List for EnforcementRepository {
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
condition, conditions, created, resolved_at
|
||||
FROM enforcement
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
@@ -316,7 +270,7 @@ impl Create for EnforcementRepository {
|
||||
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, updated
|
||||
condition, conditions, created, resolved_at
|
||||
"#,
|
||||
)
|
||||
.bind(input.rule)
|
||||
@@ -363,14 +317,23 @@ impl Update for EnforcementRepository {
|
||||
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 {
|
||||
// No updates requested, fetch and return existing entity
|
||||
return Self::get_by_id(executor, id).await;
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ");
|
||||
query.push(" WHERE id = ");
|
||||
query.push_bind(id);
|
||||
query.push(" RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload, condition, conditions, created, updated");
|
||||
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>()
|
||||
@@ -405,7 +368,7 @@ impl EnforcementRepository {
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
condition, conditions, created, resolved_at
|
||||
FROM enforcement
|
||||
WHERE rule = $1
|
||||
ORDER BY created DESC
|
||||
@@ -429,7 +392,7 @@ impl EnforcementRepository {
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
condition, conditions, created, resolved_at
|
||||
FROM enforcement
|
||||
WHERE status = $1
|
||||
ORDER BY created DESC
|
||||
@@ -450,7 +413,7 @@ impl EnforcementRepository {
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
condition, conditions, created, resolved_at
|
||||
FROM enforcement
|
||||
WHERE event = $1
|
||||
ORDER BY created DESC
|
||||
|
||||
@@ -6,6 +6,15 @@ use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
/// Column list for SELECT queries on the execution table.
|
||||
///
|
||||
/// Defined once to avoid drift between queries and the `Execution` model.
|
||||
/// The execution table has DB-only columns (`is_workflow`, `workflow_def`) that
|
||||
/// are NOT in the Rust struct, so `SELECT *` must never be used.
|
||||
pub const SELECT_COLUMNS: &str = "\
|
||||
id, action, action_ref, config, env_vars, parent, enforcement, \
|
||||
executor, status, result, workflow_task, created, updated";
|
||||
|
||||
pub struct ExecutionRepository;
|
||||
|
||||
impl Repository for ExecutionRepository {
|
||||
@@ -54,9 +63,12 @@ impl FindById for ExecutionRepository {
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated FROM execution WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
let sql = format!("SELECT {SELECT_COLUMNS} FROM execution WHERE id = $1");
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,9 +78,12 @@ impl List for ExecutionRepository {
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated FROM execution ORDER BY created DESC LIMIT 1000"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
let sql =
|
||||
format!("SELECT {SELECT_COLUMNS} FROM execution ORDER BY created DESC LIMIT 1000");
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +94,26 @@ impl Create for ExecutionRepository {
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"INSERT INTO execution (action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated"
|
||||
).bind(input.action).bind(&input.action_ref).bind(&input.config).bind(&input.env_vars).bind(input.parent).bind(input.enforcement).bind(input.executor).bind(input.status).bind(&input.result).bind(sqlx::types::Json(&input.workflow_task)).fetch_one(executor).await.map_err(Into::into)
|
||||
let sql = format!(
|
||||
"INSERT INTO execution \
|
||||
(action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \
|
||||
RETURNING {SELECT_COLUMNS}"
|
||||
);
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.bind(input.action)
|
||||
.bind(&input.action_ref)
|
||||
.bind(&input.config)
|
||||
.bind(&input.env_vars)
|
||||
.bind(input.parent)
|
||||
.bind(input.enforcement)
|
||||
.bind(input.executor)
|
||||
.bind(input.status)
|
||||
.bind(&input.result)
|
||||
.bind(sqlx::types::Json(&input.workflow_task))
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +162,8 @@ impl Update for ExecutionRepository {
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||
query.push(" RETURNING id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated");
|
||||
query.push(" RETURNING ");
|
||||
query.push(SELECT_COLUMNS);
|
||||
|
||||
query
|
||||
.build_query_as::<Execution>()
|
||||
@@ -162,9 +195,14 @@ impl ExecutionRepository {
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated FROM execution WHERE status = $1 ORDER BY created DESC"
|
||||
).bind(status).fetch_all(executor).await.map_err(Into::into)
|
||||
let sql = format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM execution WHERE status = $1 ORDER BY created DESC"
|
||||
);
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.bind(status)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_by_enforcement<'e, E>(
|
||||
@@ -174,8 +212,31 @@ impl ExecutionRepository {
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, env_vars, parent, enforcement, executor, status, result, workflow_task, created, updated FROM execution WHERE enforcement = $1 ORDER BY created DESC"
|
||||
).bind(enforcement_id).fetch_all(executor).await.map_err(Into::into)
|
||||
let sql = format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM execution WHERE enforcement = $1 ORDER BY created DESC"
|
||||
);
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.bind(enforcement_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find all child executions for a given parent execution ID.
|
||||
///
|
||||
/// Returns child executions ordered by creation time (ascending),
|
||||
/// which is the natural task execution order for workflows.
|
||||
pub async fn find_by_parent<'e, E>(executor: E, parent_id: Id) -> Result<Vec<Execution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sql = format!(
|
||||
"SELECT {SELECT_COLUMNS} FROM execution WHERE parent = $1 ORDER BY created ASC"
|
||||
);
|
||||
sqlx::query_as::<_, Execution>(&sql)
|
||||
.bind(parent_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ impl WorkflowRegistrar {
|
||||
///
|
||||
/// This ensures the workflow appears in action lists and the action palette
|
||||
/// in the workflow builder. The action is linked to the workflow definition
|
||||
/// via `is_workflow = true` and `workflow_def` FK.
|
||||
/// via the `workflow_def` FK.
|
||||
async fn create_companion_action(
|
||||
&self,
|
||||
workflow_def_id: i64,
|
||||
@@ -221,7 +221,7 @@ impl WorkflowRegistrar {
|
||||
|
||||
let action = ActionRepository::create(&self.pool, action_input).await?;
|
||||
|
||||
// Link the action to the workflow definition (sets is_workflow = true and workflow_def)
|
||||
// Link the action to the workflow definition (sets workflow_def FK)
|
||||
ActionRepository::link_workflow_def(&self.pool, action.id, workflow_def_id).await?;
|
||||
|
||||
info!(
|
||||
|
||||
@@ -54,8 +54,8 @@ async fn test_create_enforcement_minimal() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -89,7 +89,7 @@ async fn test_create_enforcement_minimal() {
|
||||
assert_eq!(enforcement.condition, EnforcementCondition::All);
|
||||
assert_eq!(enforcement.conditions, json!([]));
|
||||
assert!(enforcement.created.timestamp() > 0);
|
||||
assert!(enforcement.updated.timestamp() > 0);
|
||||
assert_eq!(enforcement.resolved_at, None); // Not yet resolved
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -125,8 +125,8 @@ async fn test_create_enforcement_with_event() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -192,8 +192,8 @@ async fn test_create_enforcement_with_conditions() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -257,8 +257,8 @@ async fn test_create_enforcement_with_any_condition() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -333,10 +333,12 @@ async fn test_create_enforcement_with_invalid_rule_fails() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_enforcement_with_invalid_event_fails() {
|
||||
async fn test_create_enforcement_with_nonexistent_event_succeeds() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
// Try to create enforcement with non-existent event ID
|
||||
// The enforcement.event column has no FK constraint (event is a hypertable
|
||||
// and hypertables cannot be FK targets). A non-existent event ID is accepted
|
||||
// as a dangling reference.
|
||||
let input = CreateEnforcementInput {
|
||||
rule: None,
|
||||
rule_ref: "some.rule".to_string(),
|
||||
@@ -351,8 +353,9 @@ async fn test_create_enforcement_with_invalid_event_fails() {
|
||||
|
||||
let result = EnforcementRepository::create(&pool, input).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
// Foreign key constraint violation
|
||||
assert!(result.is_ok());
|
||||
let enforcement = result.unwrap();
|
||||
assert_eq!(enforcement.event, Some(99999));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -392,8 +395,8 @@ async fn test_find_enforcement_by_id() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -464,8 +467,8 @@ async fn test_get_enforcement_by_id() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -542,8 +545,8 @@ async fn test_list_enforcements() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -613,8 +616,8 @@ async fn test_update_enforcement_status() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -628,9 +631,11 @@ async fn test_update_enforcement_status() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let input = UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Processed),
|
||||
payload: None,
|
||||
resolved_at: Some(now),
|
||||
};
|
||||
|
||||
let updated = EnforcementRepository::update(&pool, enforcement.id, input)
|
||||
@@ -639,7 +644,8 @@ async fn test_update_enforcement_status() {
|
||||
|
||||
assert_eq!(updated.id, enforcement.id);
|
||||
assert_eq!(updated.status, EnforcementStatus::Processed);
|
||||
assert!(updated.updated > enforcement.updated);
|
||||
assert!(updated.resolved_at.is_some());
|
||||
assert!(updated.resolved_at.unwrap() >= enforcement.created);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -675,8 +681,8 @@ async fn test_update_enforcement_status_transitions() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -689,26 +695,30 @@ async fn test_update_enforcement_status_transitions() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test status transitions: Created -> Succeeded
|
||||
// Test status transitions: Created -> Processed
|
||||
let now = chrono::Utc::now();
|
||||
let updated = EnforcementRepository::update(
|
||||
&pool,
|
||||
enforcement.id,
|
||||
UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Processed),
|
||||
payload: None,
|
||||
resolved_at: Some(now),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.status, EnforcementStatus::Processed);
|
||||
assert!(updated.resolved_at.is_some());
|
||||
|
||||
// Test status transition: Succeeded -> Failed (although unusual)
|
||||
// Test status transition: Processed -> Disabled (although unusual)
|
||||
let updated = EnforcementRepository::update(
|
||||
&pool,
|
||||
enforcement.id,
|
||||
UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Disabled),
|
||||
payload: None,
|
||||
resolved_at: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -749,8 +759,8 @@ async fn test_update_enforcement_payload() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -768,6 +778,7 @@ async fn test_update_enforcement_payload() {
|
||||
let input = UpdateEnforcementInput {
|
||||
status: None,
|
||||
payload: Some(new_payload.clone()),
|
||||
resolved_at: None,
|
||||
};
|
||||
|
||||
let updated = EnforcementRepository::update(&pool, enforcement.id, input)
|
||||
@@ -810,8 +821,8 @@ async fn test_update_enforcement_both_fields() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -824,10 +835,12 @@ async fn test_update_enforcement_both_fields() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let new_payload = json!({"result": "success"});
|
||||
let input = UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Processed),
|
||||
payload: Some(new_payload.clone()),
|
||||
resolved_at: Some(now),
|
||||
};
|
||||
|
||||
let updated = EnforcementRepository::update(&pool, enforcement.id, input)
|
||||
@@ -871,8 +884,8 @@ async fn test_update_enforcement_no_changes() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -889,6 +902,7 @@ async fn test_update_enforcement_no_changes() {
|
||||
let input = UpdateEnforcementInput {
|
||||
status: None,
|
||||
payload: None,
|
||||
resolved_at: None,
|
||||
};
|
||||
|
||||
let result = EnforcementRepository::update(&pool, enforcement.id, input)
|
||||
@@ -907,6 +921,7 @@ async fn test_update_enforcement_not_found() {
|
||||
let input = UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Processed),
|
||||
payload: None,
|
||||
resolved_at: Some(chrono::Utc::now()),
|
||||
};
|
||||
|
||||
let result = EnforcementRepository::update(&pool, 99999, input).await;
|
||||
@@ -952,8 +967,8 @@ async fn test_delete_enforcement() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1025,8 +1040,8 @@ async fn test_find_enforcements_by_rule() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1047,8 +1062,8 @@ async fn test_find_enforcements_by_rule() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1117,8 +1132,8 @@ async fn test_find_enforcements_by_status() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1206,8 +1221,8 @@ async fn test_find_enforcements_by_event() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1290,8 +1305,8 @@ async fn test_delete_rule_sets_enforcement_rule_to_null() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1323,7 +1338,7 @@ async fn test_delete_rule_sets_enforcement_rule_to_null() {
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_enforcement_timestamps_auto_managed() {
|
||||
async fn test_enforcement_resolved_at_lifecycle() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("timestamp_pack")
|
||||
@@ -1355,8 +1370,8 @@ async fn test_enforcement_timestamps_auto_managed() {
|
||||
trigger: trigger.id,
|
||||
trigger_ref: trigger.r#ref.clone(),
|
||||
conditions: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
action_params: json!({}),
|
||||
trigger_params: json!({}),
|
||||
enabled: true,
|
||||
is_adhoc: false,
|
||||
},
|
||||
@@ -1369,24 +1384,23 @@ async fn test_enforcement_timestamps_auto_managed() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let created_time = enforcement.created;
|
||||
let updated_time = enforcement.updated;
|
||||
|
||||
assert!(created_time.timestamp() > 0);
|
||||
assert_eq!(created_time, updated_time);
|
||||
|
||||
// Update and verify timestamp changed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
// Initially, resolved_at is NULL
|
||||
assert!(enforcement.created.timestamp() > 0);
|
||||
assert_eq!(enforcement.resolved_at, None);
|
||||
|
||||
// Resolve the enforcement and verify resolved_at is set
|
||||
let resolved_time = chrono::Utc::now();
|
||||
let input = UpdateEnforcementInput {
|
||||
status: Some(EnforcementStatus::Processed),
|
||||
payload: None,
|
||||
resolved_at: Some(resolved_time),
|
||||
};
|
||||
|
||||
let updated = EnforcementRepository::update(&pool, enforcement.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.created, created_time); // created unchanged
|
||||
assert!(updated.updated > updated_time); // updated changed
|
||||
assert_eq!(updated.created, enforcement.created); // created unchanged
|
||||
assert!(updated.resolved_at.is_some());
|
||||
assert!(updated.resolved_at.unwrap() >= enforcement.created);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
//!
|
||||
//! These tests verify CRUD operations, queries, and constraints
|
||||
//! for the Event repository.
|
||||
//! Note: Events are immutable time-series data — there are no update tests.
|
||||
|
||||
mod helpers;
|
||||
|
||||
use attune_common::{
|
||||
repositories::{
|
||||
event::{CreateEventInput, EventRepository, UpdateEventInput},
|
||||
Create, Delete, FindById, List, Update,
|
||||
event::{CreateEventInput, EventRepository},
|
||||
Create, Delete, FindById, List,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
@@ -56,7 +57,6 @@ async fn test_create_event_minimal() {
|
||||
assert_eq!(event.source, None);
|
||||
assert_eq!(event.source_ref, None);
|
||||
assert!(event.created.timestamp() > 0);
|
||||
assert!(event.updated.timestamp() > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -363,162 +363,6 @@ async fn test_list_events_respects_limit() {
|
||||
assert!(events.len() <= 1000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UPDATE Tests
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_event_config() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("update_pack")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref)
|
||||
.with_config(json!({"old": "config"}))
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_config = json!({"new": "config", "updated": true});
|
||||
let input = UpdateEventInput {
|
||||
config: Some(new_config.clone()),
|
||||
payload: None,
|
||||
};
|
||||
|
||||
let updated = EventRepository::update(&pool, event.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.id, event.id);
|
||||
assert_eq!(updated.config, Some(new_config));
|
||||
assert!(updated.updated > event.updated);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_event_payload() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("payload_update_pack")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref)
|
||||
.with_payload(json!({"initial": "payload"}))
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_payload = json!({"updated": "payload", "version": 2});
|
||||
let input = UpdateEventInput {
|
||||
config: None,
|
||||
payload: Some(new_payload.clone()),
|
||||
};
|
||||
|
||||
let updated = EventRepository::update(&pool, event.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.payload, Some(new_payload));
|
||||
assert!(updated.updated > event.updated);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_event_both_fields() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("both_update_pack")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref)
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_config = json!({"setting": "value"});
|
||||
let new_payload = json!({"data": "value"});
|
||||
let input = UpdateEventInput {
|
||||
config: Some(new_config.clone()),
|
||||
payload: Some(new_payload.clone()),
|
||||
};
|
||||
|
||||
let updated = EventRepository::update(&pool, event.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.config, Some(new_config));
|
||||
assert_eq!(updated.payload, Some(new_payload));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_event_no_changes() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("nochange_pack")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trigger = TriggerFixture::new_unique(Some(pack.id), Some(pack.r#ref.clone()), "webhook")
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = EventFixture::new_unique(Some(trigger.id), &trigger.r#ref)
|
||||
.with_payload(json!({"test": "data"}))
|
||||
.create(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let input = UpdateEventInput {
|
||||
config: None,
|
||||
payload: None,
|
||||
};
|
||||
|
||||
let result = EventRepository::update(&pool, event.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should return existing event without updating
|
||||
assert_eq!(result.id, event.id);
|
||||
assert_eq!(result.payload, event.payload);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_event_not_found() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let input = UpdateEventInput {
|
||||
config: Some(json!({"test": "config"})),
|
||||
payload: None,
|
||||
};
|
||||
|
||||
let result = EventRepository::update(&pool, 99999, input).await;
|
||||
|
||||
// When updating non-existent entity with changes, SQLx returns RowNotFound error
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DELETE Tests
|
||||
// ============================================================================
|
||||
@@ -561,7 +405,7 @@ async fn test_delete_event_not_found() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_event_sets_enforcement_event_to_null() {
|
||||
async fn test_delete_event_enforcement_retains_event_id() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
// Create pack, trigger, action, rule, and event
|
||||
@@ -616,17 +460,19 @@ async fn test_delete_event_sets_enforcement_event_to_null() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete the event - enforcement.event should be set to NULL (ON DELETE SET NULL)
|
||||
// Delete the event — since the event table is a TimescaleDB hypertable, the FK
|
||||
// constraint from enforcement.event was dropped (hypertables cannot be FK targets).
|
||||
// The enforcement.event column retains the old ID as a dangling reference.
|
||||
EventRepository::delete(&pool, event.id).await.unwrap();
|
||||
|
||||
// Enforcement should still exist but with NULL event
|
||||
// Enforcement still exists with the original event ID (now a dangling reference)
|
||||
use attune_common::repositories::event::EnforcementRepository;
|
||||
let found_enforcement = EnforcementRepository::find_by_id(&pool, enforcement.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(found_enforcement.event, None);
|
||||
assert_eq!(found_enforcement.event, Some(event.id));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -756,7 +602,7 @@ async fn test_find_events_by_trigger_ref_preserves_after_trigger_deletion() {
|
||||
// ============================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_event_timestamps_auto_managed() {
|
||||
async fn test_event_created_timestamp_auto_set() {
|
||||
let pool = create_test_pool().await.unwrap();
|
||||
|
||||
let pack = PackFixture::new_unique("timestamp_pack")
|
||||
@@ -774,24 +620,5 @@ async fn test_event_timestamps_auto_managed() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let created_time = event.created;
|
||||
let updated_time = event.updated;
|
||||
|
||||
assert!(created_time.timestamp() > 0);
|
||||
assert_eq!(created_time, updated_time);
|
||||
|
||||
// Update and verify timestamp changed
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
|
||||
let input = UpdateEventInput {
|
||||
config: Some(json!({"updated": true})),
|
||||
payload: None,
|
||||
};
|
||||
|
||||
let updated = EventRepository::update(&pool, event.id, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.created, created_time); // created unchanged
|
||||
assert!(updated.updated > updated_time); // updated changed
|
||||
assert!(event.created.timestamp() > 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user