Files
attune/crates/api/src/dto/execution.rs

349 lines
11 KiB
Rust

//! Execution DTOs for API requests and responses
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use utoipa::{IntoParams, ToSchema};
use attune_common::models::enums::ExecutionStatus;
use attune_common::models::execution::WorkflowTaskMetadata;
use attune_common::repositories::execution::ExecutionWithRefs;
/// Request DTO for creating a manual execution
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct CreateExecutionRequest {
/// Action reference to execute
#[schema(example = "slack.post_message")]
pub action_ref: String,
/// Execution parameters/configuration
#[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "Manual test"}))]
pub parameters: Option<JsonValue>,
/// Environment variables for this execution
#[schema(value_type = Object, example = json!({"DEBUG": "true", "LOG_LEVEL": "info"}))]
pub env_vars: Option<JsonValue>,
}
/// Response DTO for execution information
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ExecutionResponse {
/// Execution ID
#[schema(example = 1)]
pub id: i64,
/// Action ID (optional, may be null for ad-hoc executions)
#[schema(example = 1)]
pub action: Option<i64>,
/// Action reference
#[schema(example = "slack.post_message")]
pub action_ref: String,
/// Execution configuration/parameters
#[schema(value_type = Object, example = json!({"channel": "#alerts", "message": "System error detected"}))]
pub config: Option<JsonValue>,
/// Parent execution ID (for nested/child executions)
#[schema(example = 1)]
pub parent: Option<i64>,
/// Enforcement ID (rule enforcement that triggered this)
#[schema(example = 1)]
pub enforcement: Option<i64>,
/// Identity ID that initiated this execution
#[schema(example = 1)]
pub executor: Option<i64>,
/// Worker ID currently assigned to this execution
#[schema(example = 1)]
pub worker: Option<i64>,
/// Execution status
#[schema(example = "succeeded")]
pub status: ExecutionStatus,
/// Execution result/output
#[schema(value_type = Object, example = json!({"message_id": "1234567890.123456"}))]
pub result: Option<JsonValue>,
/// When the execution actually started running (worker picked it up).
/// Null if the execution hasn't started running yet.
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(example = "2024-01-13T10:31:00Z", nullable = true)]
pub started_at: Option<DateTime<Utc>>,
/// Workflow task metadata (only populated for workflow task executions)
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Object>, nullable = true)]
pub workflow_task: Option<WorkflowTaskMetadata>,
/// Creation timestamp
#[schema(example = "2024-01-13T10:30:00Z")]
pub created: DateTime<Utc>,
/// Last update timestamp
#[schema(example = "2024-01-13T10:35:00Z")]
pub updated: DateTime<Utc>,
}
/// Simplified execution response (for list endpoints)
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ExecutionSummary {
/// Execution ID
#[schema(example = 1)]
pub id: i64,
/// Action reference
#[schema(example = "slack.post_message")]
pub action_ref: String,
/// Execution status
#[schema(example = "succeeded")]
pub status: ExecutionStatus,
/// Parent execution ID
#[schema(example = 1)]
pub parent: Option<i64>,
/// Enforcement ID
#[schema(example = 1)]
pub enforcement: Option<i64>,
/// Rule reference (if triggered by a rule)
#[schema(example = "core.on_timer")]
pub rule_ref: Option<String>,
/// Trigger reference (if triggered by a trigger)
#[schema(example = "core.timer")]
pub trigger_ref: Option<String>,
/// When the execution actually started running (worker picked it up).
/// Null if the execution hasn't started running yet.
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(example = "2024-01-13T10:31:00Z", nullable = true)]
pub started_at: Option<DateTime<Utc>>,
/// Workflow task metadata (only populated for workflow task executions)
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<Object>, nullable = true)]
pub workflow_task: Option<WorkflowTaskMetadata>,
/// Creation timestamp
#[schema(example = "2024-01-13T10:30:00Z")]
pub created: DateTime<Utc>,
/// Last update timestamp
#[schema(example = "2024-01-13T10:35:00Z")]
pub updated: DateTime<Utc>,
}
/// Query parameters for filtering executions
#[derive(Debug, Clone, Deserialize, IntoParams)]
pub struct ExecutionQueryParams {
/// Filter by execution status
#[param(example = "succeeded")]
pub status: Option<ExecutionStatus>,
/// Filter by action reference
#[param(example = "slack.post_message")]
pub action_ref: Option<String>,
/// Filter by pack name
#[param(example = "core")]
pub pack_name: Option<String>,
/// Filter by rule reference
#[param(example = "core.on_timer")]
pub rule_ref: Option<String>,
/// Filter by trigger reference
#[param(example = "core.timer")]
pub trigger_ref: Option<String>,
/// Filter by executor ID
#[param(example = 1)]
pub executor: Option<i64>,
/// Search in result JSON (case-insensitive substring match)
#[param(example = "error")]
pub result_contains: Option<String>,
/// Filter by enforcement ID
#[param(example = 1)]
pub enforcement: Option<i64>,
/// Filter by parent execution ID
#[param(example = 1)]
pub parent: Option<i64>,
/// If true, only return top-level executions (those without a parent).
/// Useful for the "By Workflow" view where child tasks are loaded separately.
#[serde(default)]
#[param(example = false)]
pub top_level_only: Option<bool>,
/// Page number (for pagination)
#[serde(default = "default_page")]
#[param(example = 1, minimum = 1)]
pub page: u32,
/// Items per page (for pagination)
#[serde(default = "default_per_page")]
#[param(example = 50, minimum = 1, maximum = 100)]
pub per_page: u32,
}
impl ExecutionQueryParams {
/// Get the SQL offset value
pub fn offset(&self) -> u32 {
(self.page.saturating_sub(1)) * self.per_page
}
/// Get the limit value (with max cap)
pub fn limit(&self) -> u32 {
self.per_page.min(100)
}
}
/// Convert from Execution model to ExecutionResponse
impl From<attune_common::models::execution::Execution> for ExecutionResponse {
fn from(execution: attune_common::models::execution::Execution) -> Self {
Self {
id: execution.id,
action: execution.action,
action_ref: execution.action_ref,
config: execution
.config
.map(|c| serde_json::to_value(c).unwrap_or(JsonValue::Null)),
parent: execution.parent,
enforcement: execution.enforcement,
executor: execution.executor,
worker: execution.worker,
status: execution.status,
result: execution
.result
.map(|r| serde_json::to_value(r).unwrap_or(JsonValue::Null)),
started_at: execution.started_at,
workflow_task: execution.workflow_task,
created: execution.created,
updated: execution.updated,
}
}
}
/// Convert from Execution model to ExecutionSummary
impl From<attune_common::models::execution::Execution> for ExecutionSummary {
fn from(execution: attune_common::models::execution::Execution) -> Self {
Self {
id: execution.id,
action_ref: execution.action_ref,
status: execution.status,
parent: execution.parent,
enforcement: execution.enforcement,
rule_ref: None, // Populated separately via enforcement lookup
trigger_ref: None, // Populated separately via enforcement lookup
started_at: execution.started_at,
workflow_task: execution.workflow_task,
created: execution.created,
updated: execution.updated,
}
}
}
/// Convert from the joined query result (execution + enforcement refs).
/// `rule_ref` and `trigger_ref` are already populated from the SQL JOIN.
impl From<ExecutionWithRefs> for ExecutionSummary {
fn from(row: ExecutionWithRefs) -> Self {
Self {
id: row.id,
action_ref: row.action_ref,
status: row.status,
parent: row.parent,
enforcement: row.enforcement,
rule_ref: row.rule_ref,
trigger_ref: row.trigger_ref,
started_at: row.started_at,
workflow_task: row.workflow_task,
created: row.created,
updated: row.updated,
}
}
}
fn default_page() -> u32 {
1
}
fn default_per_page() -> u32 {
20
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_params_defaults() {
let json = r#"{}"#;
let params: ExecutionQueryParams = serde_json::from_str(json).unwrap();
assert_eq!(params.page, 1);
assert_eq!(params.per_page, 20);
assert!(params.status.is_none());
}
#[test]
fn test_query_params_with_filters() {
let json = r#"{
"status": "completed",
"action_ref": "test.action",
"page": 2,
"per_page": 50
}"#;
let params: ExecutionQueryParams = serde_json::from_str(json).unwrap();
assert_eq!(params.page, 2);
assert_eq!(params.per_page, 50);
assert_eq!(params.status, Some(ExecutionStatus::Completed));
assert_eq!(params.action_ref, Some("test.action".to_string()));
}
#[test]
fn test_query_params_offset() {
let params = ExecutionQueryParams {
status: None,
action_ref: None,
enforcement: None,
parent: None,
top_level_only: None,
pack_name: None,
rule_ref: None,
trigger_ref: None,
executor: None,
result_contains: None,
page: 3,
per_page: 20,
};
assert_eq!(params.offset(), 40); // (3-1) * 20
}
#[test]
fn test_query_params_limit_cap() {
let params = ExecutionQueryParams {
status: None,
action_ref: None,
enforcement: None,
parent: None,
top_level_only: None,
pack_name: None,
rule_ref: None,
trigger_ref: None,
executor: None,
result_contains: None,
page: 1,
per_page: 200, // Exceeds max
};
assert_eq!(params.limit(), 100); // Capped at 100
}
}