[WIP] Workflows
This commit is contained in:
@@ -137,6 +137,11 @@ pub struct ActionResponse {
|
||||
#[schema(value_type = Object, nullable = true)]
|
||||
pub out_schema: Option<JsonValue>,
|
||||
|
||||
/// Workflow definition ID (non-null if this action is a workflow)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(example = 42, nullable = true)]
|
||||
pub workflow_def: Option<i64>,
|
||||
|
||||
/// Whether this is an ad-hoc action (not from pack installation)
|
||||
#[schema(example = false)]
|
||||
pub is_adhoc: bool,
|
||||
@@ -186,6 +191,11 @@ pub struct ActionSummary {
|
||||
#[schema(example = ">=3.12", nullable = true)]
|
||||
pub runtime_version_constraint: Option<String>,
|
||||
|
||||
/// Workflow definition ID (non-null if this action is a workflow)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(example = 42, nullable = true)]
|
||||
pub workflow_def: Option<i64>,
|
||||
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
@@ -210,6 +220,7 @@ impl From<attune_common::models::action::Action> for ActionResponse {
|
||||
runtime_version_constraint: action.runtime_version_constraint,
|
||||
param_schema: action.param_schema,
|
||||
out_schema: action.out_schema,
|
||||
workflow_def: action.workflow_def,
|
||||
is_adhoc: action.is_adhoc,
|
||||
created: action.created,
|
||||
updated: action.updated,
|
||||
@@ -229,6 +240,7 @@ impl From<attune_common::models::action::Action> for ActionSummary {
|
||||
entrypoint: action.entrypoint,
|
||||
runtime: action.runtime,
|
||||
runtime_version_constraint: action.runtime_version_constraint,
|
||||
workflow_def: action.workflow_def,
|
||||
created: action.created,
|
||||
updated: action.updated,
|
||||
}
|
||||
|
||||
@@ -53,10 +53,6 @@ pub struct EventResponse {
|
||||
/// Creation timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<Event> for EventResponse {
|
||||
@@ -72,7 +68,6 @@ impl From<Event> for EventResponse {
|
||||
rule: event.rule,
|
||||
rule_ref: event.rule_ref,
|
||||
created: event.created,
|
||||
updated: event.updated,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,9 +225,9 @@ pub struct EnforcementResponse {
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub created: DateTime<Utc>,
|
||||
|
||||
/// Last update timestamp
|
||||
#[schema(example = "2024-01-13T10:30:00Z")]
|
||||
pub updated: DateTime<Utc>,
|
||||
/// Timestamp when the enforcement was resolved (status changed from created to processed/disabled)
|
||||
#[schema(example = "2024-01-13T10:30:01Z", nullable = true)]
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl From<Enforcement> for EnforcementResponse {
|
||||
@@ -249,7 +244,7 @@ impl From<Enforcement> for EnforcementResponse {
|
||||
condition: enforcement.condition,
|
||||
conditions: enforcement.conditions,
|
||||
created: enforcement.created,
|
||||
updated: enforcement.updated,
|
||||
resolved_at: enforcement.resolved_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use serde_json::Value as JsonValue;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use attune_common::models::enums::ExecutionStatus;
|
||||
use attune_common::models::execution::WorkflowTaskMetadata;
|
||||
|
||||
/// Request DTO for creating a manual execution
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
@@ -62,6 +63,11 @@ pub struct ExecutionResponse {
|
||||
#[schema(value_type = Object, example = json!({"message_id": "1234567890.123456"}))]
|
||||
pub result: Option<JsonValue>,
|
||||
|
||||
/// 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>,
|
||||
@@ -102,6 +108,11 @@ pub struct ExecutionSummary {
|
||||
#[schema(example = "core.timer")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// 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>,
|
||||
@@ -150,6 +161,12 @@ pub struct ExecutionQueryParams {
|
||||
#[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)]
|
||||
@@ -190,6 +207,7 @@ impl From<attune_common::models::execution::Execution> for ExecutionResponse {
|
||||
result: execution
|
||||
.result
|
||||
.map(|r| serde_json::to_value(r).unwrap_or(JsonValue::Null)),
|
||||
workflow_task: execution.workflow_task,
|
||||
created: execution.created,
|
||||
updated: execution.updated,
|
||||
}
|
||||
@@ -207,6 +225,7 @@ impl From<attune_common::models::execution::Execution> for ExecutionSummary {
|
||||
enforcement: execution.enforcement,
|
||||
rule_ref: None, // Populated separately via enforcement lookup
|
||||
trigger_ref: None, // Populated separately via enforcement lookup
|
||||
workflow_task: execution.workflow_task,
|
||||
created: execution.created,
|
||||
updated: execution.updated,
|
||||
}
|
||||
@@ -256,6 +275,7 @@ mod tests {
|
||||
action_ref: None,
|
||||
enforcement: None,
|
||||
parent: None,
|
||||
top_level_only: None,
|
||||
pack_name: None,
|
||||
rule_ref: None,
|
||||
trigger_ref: None,
|
||||
@@ -274,6 +294,7 @@ mod tests {
|
||||
action_ref: None,
|
||||
enforcement: None,
|
||||
parent: None,
|
||||
top_level_only: None,
|
||||
pack_name: None,
|
||||
rule_ref: None,
|
||||
trigger_ref: None,
|
||||
|
||||
@@ -126,7 +126,7 @@ impl HistoryQueryParams {
|
||||
/// Path parameter for the entity type segment.
|
||||
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||||
pub struct HistoryEntityTypePath {
|
||||
/// Entity type: `execution`, `worker`, `enforcement`, or `event`
|
||||
/// Entity type: `execution` or `worker`
|
||||
pub entity_type: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,10 @@ pub async fn list_executions(
|
||||
filtered_executions.retain(|e| e.parent == Some(parent_id));
|
||||
}
|
||||
|
||||
if query.top_level_only == Some(true) {
|
||||
filtered_executions.retain(|e| e.parent.is_none());
|
||||
}
|
||||
|
||||
if let Some(executor_id) = query.executor {
|
||||
filtered_executions.retain(|e| e.executor == Some(executor_id));
|
||||
}
|
||||
|
||||
@@ -27,14 +27,14 @@ use crate::{
|
||||
|
||||
/// List history records for a given entity type.
|
||||
///
|
||||
/// Supported entity types: `execution`, `worker`, `enforcement`, `event`.
|
||||
/// Supported entity types: `execution`, `worker`.
|
||||
/// Returns a paginated list of change records ordered by time descending.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/history/{entity_type}",
|
||||
tag = "history",
|
||||
params(
|
||||
("entity_type" = String, Path, description = "Entity type: execution, worker, enforcement, or event"),
|
||||
("entity_type" = String, Path, description = "Entity type: execution or worker"),
|
||||
HistoryQueryParams,
|
||||
),
|
||||
responses(
|
||||
@@ -127,56 +127,6 @@ pub async fn get_worker_history(
|
||||
get_entity_history_by_id(&state, HistoryEntityType::Worker, id, query).await
|
||||
}
|
||||
|
||||
/// Get history for a specific enforcement by ID.
|
||||
///
|
||||
/// Returns all change records for the given enforcement, ordered by time descending.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/enforcements/{id}/history",
|
||||
tag = "history",
|
||||
params(
|
||||
("id" = i64, Path, description = "Enforcement ID"),
|
||||
HistoryQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "History records for the enforcement", body = PaginatedResponse<HistoryRecordResponse>),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_enforcement_history(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Path(id): Path<i64>,
|
||||
Query(query): Query<HistoryQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
get_entity_history_by_id(&state, HistoryEntityType::Enforcement, id, query).await
|
||||
}
|
||||
|
||||
/// Get history for a specific event by ID.
|
||||
///
|
||||
/// Returns all change records for the given event, ordered by time descending.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/events/{id}/history",
|
||||
tag = "history",
|
||||
params(
|
||||
("id" = i64, Path, description = "Event ID"),
|
||||
HistoryQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "History records for the event", body = PaginatedResponse<HistoryRecordResponse>),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_event_history(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Path(id): Path<i64>,
|
||||
Query(query): Query<HistoryQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
get_entity_history_by_id(&state, HistoryEntityType::Event, id, query).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -231,8 +181,6 @@ async fn get_entity_history_by_id(
|
||||
/// - `GET /history/:entity_type` — generic history query
|
||||
/// - `GET /executions/:id/history` — execution-specific history
|
||||
/// - `GET /workers/:id/history` — worker-specific history (note: currently no /workers base route exists)
|
||||
/// - `GET /enforcements/:id/history` — enforcement-specific history
|
||||
/// - `GET /events/:id/history` — event-specific history
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
// Generic history endpoint
|
||||
@@ -240,6 +188,4 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
// Entity-specific convenience endpoints
|
||||
.route("/executions/{id}/history", get(get_execution_history))
|
||||
.route("/workers/{id}/history", get(get_worker_history))
|
||||
.route("/enforcements/{id}/history", get(get_enforcement_history))
|
||||
.route("/events/{id}/history", get(get_event_history))
|
||||
}
|
||||
|
||||
@@ -601,8 +601,8 @@ async fn write_workflow_yaml(
|
||||
/// Create a companion action record for a workflow definition.
|
||||
///
|
||||
/// This ensures the workflow appears in action lists and the action palette in the
|
||||
/// workflow builder. The action is created with `is_workflow = true` and linked to
|
||||
/// the workflow definition via the `workflow_def` FK.
|
||||
/// workflow builder. The action is linked to the workflow definition via the
|
||||
/// `workflow_def` FK.
|
||||
async fn create_companion_action(
|
||||
db: &sqlx::PgPool,
|
||||
workflow_ref: &str,
|
||||
@@ -643,7 +643,7 @@ async fn create_companion_action(
|
||||
))
|
||||
})?;
|
||||
|
||||
// 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(db, action.id, workflow_def_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
||||
@@ -368,7 +368,6 @@ mod tests {
|
||||
runtime_version_constraint: None,
|
||||
param_schema: schema,
|
||||
out_schema: None,
|
||||
is_workflow: false,
|
||||
workflow_def: None,
|
||||
is_adhoc: false,
|
||||
parameter_delivery: attune_common::models::ParameterDelivery::default(),
|
||||
|
||||
@@ -120,23 +120,21 @@ async fn test_sse_stream_receives_execution_updates() -> Result<()> {
|
||||
println!("Updating execution {} to 'running' status", execution_id);
|
||||
|
||||
// Update execution status - this should trigger PostgreSQL NOTIFY
|
||||
let _ = sqlx::query(
|
||||
"UPDATE execution SET status = 'running', start_time = NOW() WHERE id = $1",
|
||||
)
|
||||
.bind(execution_id)
|
||||
.execute(&pool_clone)
|
||||
.await;
|
||||
let _ =
|
||||
sqlx::query("UPDATE execution SET status = 'running', updated = NOW() WHERE id = $1")
|
||||
.bind(execution_id)
|
||||
.execute(&pool_clone)
|
||||
.await;
|
||||
|
||||
println!("Update executed, waiting before setting to succeeded");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
// Update to succeeded
|
||||
let _ = sqlx::query(
|
||||
"UPDATE execution SET status = 'succeeded', end_time = NOW() WHERE id = $1",
|
||||
)
|
||||
.bind(execution_id)
|
||||
.execute(&pool_clone)
|
||||
.await;
|
||||
let _ =
|
||||
sqlx::query("UPDATE execution SET status = 'succeeded', updated = NOW() WHERE id = $1")
|
||||
.bind(execution_id)
|
||||
.execute(&pool_clone)
|
||||
.await;
|
||||
|
||||
println!("Execution {} updated to 'succeeded'", execution_id);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user