proper sql filtering
This commit is contained in:
@@ -319,6 +319,10 @@ pub struct EnforcementQueryParams {
|
||||
#[param(example = "core.webhook")]
|
||||
pub trigger_ref: Option<String>,
|
||||
|
||||
/// Filter by rule reference
|
||||
#[param(example = "core.on_webhook")]
|
||||
pub rule_ref: Option<String>,
|
||||
|
||||
/// Page number (1-indexed)
|
||||
#[serde(default = "default_page")]
|
||||
#[param(example = 1, minimum = 1)]
|
||||
|
||||
@@ -7,6 +7,7 @@ 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)]
|
||||
@@ -63,6 +64,12 @@ pub struct ExecutionResponse {
|
||||
#[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)]
|
||||
@@ -108,6 +115,12 @@ pub struct ExecutionSummary {
|
||||
#[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)]
|
||||
@@ -207,6 +220,7 @@ impl From<attune_common::models::execution::Execution> for ExecutionResponse {
|
||||
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,
|
||||
@@ -225,6 +239,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
|
||||
started_at: execution.started_at,
|
||||
workflow_task: execution.workflow_task,
|
||||
created: execution.created,
|
||||
updated: execution.updated,
|
||||
@@ -232,6 +247,26 @@ impl From<attune_common::models::execution::Execution> for ExecutionSummary {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
use attune_common::repositories::{
|
||||
action::{ActionRepository, CreateActionInput, UpdateActionInput},
|
||||
action::{ActionRepository, ActionSearchFilters, CreateActionInput, UpdateActionInput},
|
||||
pack::PackRepository,
|
||||
queue_stats::QueueStatsRepository,
|
||||
Create, Delete, FindByRef, List, Update,
|
||||
Create, Delete, FindByRef, Update,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -47,21 +47,20 @@ pub async fn list_actions(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get all actions (we'll implement pagination in repository later)
|
||||
let actions = ActionRepository::list(&state.db).await?;
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = ActionSearchFilters {
|
||||
pack: None,
|
||||
query: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = actions.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(actions.len());
|
||||
let result = ActionRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_actions: Vec<ActionSummary> = actions[start..end]
|
||||
.iter()
|
||||
.map(|a| ActionSummary::from(a.clone()))
|
||||
.collect();
|
||||
let paginated_actions: Vec<ActionSummary> =
|
||||
result.rows.into_iter().map(ActionSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_actions, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_actions, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -92,21 +91,20 @@ pub async fn list_actions_by_pack(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
||||
|
||||
// Get actions for this pack
|
||||
let actions = ActionRepository::find_by_pack(&state.db, pack.id).await?;
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = ActionSearchFilters {
|
||||
pack: Some(pack.id),
|
||||
query: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = actions.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(actions.len());
|
||||
let result = ActionRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_actions: Vec<ActionSummary> = actions[start..end]
|
||||
.iter()
|
||||
.map(|a| ActionSummary::from(a.clone()))
|
||||
.collect();
|
||||
let paginated_actions: Vec<ActionSummary> =
|
||||
result.rows.into_iter().map(ActionSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_actions, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_actions, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ use validator::Validate;
|
||||
use attune_common::{
|
||||
mq::{EventCreatedPayload, MessageEnvelope, MessageType},
|
||||
repositories::{
|
||||
event::{CreateEventInput, EnforcementRepository, EventRepository},
|
||||
event::{
|
||||
CreateEventInput, EnforcementRepository, EnforcementSearchFilters, EventRepository,
|
||||
EventSearchFilters,
|
||||
},
|
||||
trigger::TriggerRepository,
|
||||
Create, FindById, FindByRef, List,
|
||||
Create, FindById, FindByRef,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -220,53 +223,27 @@ pub async fn list_events(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<EventQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get events based on filters
|
||||
let events = if let Some(trigger_id) = query.trigger {
|
||||
// Filter by trigger ID
|
||||
EventRepository::find_by_trigger(&state.db, trigger_id).await?
|
||||
} else if let Some(trigger_ref) = &query.trigger_ref {
|
||||
// Filter by trigger reference
|
||||
EventRepository::find_by_trigger_ref(&state.db, trigger_ref).await?
|
||||
} else {
|
||||
// Get all events
|
||||
EventRepository::list(&state.db).await?
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = EventSearchFilters {
|
||||
trigger: query.trigger,
|
||||
trigger_ref: query.trigger_ref.clone(),
|
||||
source: query.source,
|
||||
rule_ref: query.rule_ref.clone(),
|
||||
limit: query.limit(),
|
||||
offset: query.offset(),
|
||||
};
|
||||
|
||||
// Apply additional filters in memory
|
||||
let mut filtered_events = events;
|
||||
let result = EventRepository::search(&state.db, &filters).await?;
|
||||
|
||||
if let Some(source_id) = query.source {
|
||||
filtered_events.retain(|e| e.source == Some(source_id));
|
||||
}
|
||||
let paginated_events: Vec<EventSummary> =
|
||||
result.rows.into_iter().map(EventSummary::from).collect();
|
||||
|
||||
if let Some(rule_ref) = &query.rule_ref {
|
||||
let rule_ref_lower = rule_ref.to_lowercase();
|
||||
filtered_events.retain(|e| {
|
||||
e.rule_ref
|
||||
.as_ref()
|
||||
.map(|r| r.to_lowercase().contains(&rule_ref_lower))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_events.len() as u64;
|
||||
let start = query.offset() as usize;
|
||||
let end = (start + query.limit() as usize).min(filtered_events.len());
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_events: Vec<EventSummary> = filtered_events[start..end]
|
||||
.iter()
|
||||
.map(|event| EventSummary::from(event.clone()))
|
||||
.collect();
|
||||
|
||||
// Convert query params to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: query.page,
|
||||
page_size: query.per_page,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_events, &pagination_params, total);
|
||||
let response = PaginatedResponse::new(paginated_events, &pagination_params, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -319,46 +296,32 @@ pub async fn list_enforcements(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<EnforcementQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get enforcements based on filters
|
||||
let enforcements = if let Some(status) = query.status {
|
||||
// Filter by status
|
||||
EnforcementRepository::find_by_status(&state.db, status).await?
|
||||
} else if let Some(rule_id) = query.rule {
|
||||
// Filter by rule ID
|
||||
EnforcementRepository::find_by_rule(&state.db, rule_id).await?
|
||||
} else if let Some(event_id) = query.event {
|
||||
// Filter by event ID
|
||||
EnforcementRepository::find_by_event(&state.db, event_id).await?
|
||||
} else {
|
||||
// Get all enforcements
|
||||
EnforcementRepository::list(&state.db).await?
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
// Filters are combinable (AND), not mutually exclusive.
|
||||
let filters = EnforcementSearchFilters {
|
||||
status: query.status,
|
||||
rule: query.rule,
|
||||
event: query.event,
|
||||
trigger_ref: query.trigger_ref.clone(),
|
||||
rule_ref: query.rule_ref.clone(),
|
||||
limit: query.limit(),
|
||||
offset: query.offset(),
|
||||
};
|
||||
|
||||
// Apply additional filters in memory
|
||||
let mut filtered_enforcements = enforcements;
|
||||
let result = EnforcementRepository::search(&state.db, &filters).await?;
|
||||
|
||||
if let Some(trigger_ref) = &query.trigger_ref {
|
||||
filtered_enforcements.retain(|e| e.trigger_ref == *trigger_ref);
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_enforcements.len() as u64;
|
||||
let start = query.offset() as usize;
|
||||
let end = (start + query.limit() as usize).min(filtered_enforcements.len());
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_enforcements: Vec<EnforcementSummary> = filtered_enforcements[start..end]
|
||||
.iter()
|
||||
.map(|enforcement| EnforcementSummary::from(enforcement.clone()))
|
||||
let paginated_enforcements: Vec<EnforcementSummary> = result
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(EnforcementSummary::from)
|
||||
.collect();
|
||||
|
||||
// Convert query params to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: query.page,
|
||||
page_size: query.per_page,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_enforcements, &pagination_params, total);
|
||||
let response = PaginatedResponse::new(paginated_enforcements, &pagination_params, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ use attune_common::models::enums::ExecutionStatus;
|
||||
use attune_common::mq::{ExecutionRequestedPayload, MessageEnvelope, MessageType};
|
||||
use attune_common::repositories::{
|
||||
action::ActionRepository,
|
||||
execution::{CreateExecutionInput, ExecutionRepository},
|
||||
Create, EnforcementRepository, FindById, FindByRef, List,
|
||||
execution::{CreateExecutionInput, ExecutionRepository, ExecutionSearchFilters},
|
||||
Create, FindById, FindByRef,
|
||||
};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::{
|
||||
auth::middleware::RequireAuth,
|
||||
@@ -125,117 +126,37 @@ pub async fn list_executions(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(query): Query<ExecutionQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get executions based on filters
|
||||
let executions = if let Some(status) = query.status {
|
||||
// Filter by status
|
||||
ExecutionRepository::find_by_status(&state.db, status).await?
|
||||
} else if let Some(enforcement_id) = query.enforcement {
|
||||
// Filter by enforcement
|
||||
ExecutionRepository::find_by_enforcement(&state.db, enforcement_id).await?
|
||||
} else {
|
||||
// Get all executions
|
||||
ExecutionRepository::list(&state.db).await?
|
||||
// All filtering, pagination, and the enforcement JOIN happen in a single
|
||||
// SQL query — no in-memory filtering or post-fetch lookups.
|
||||
let filters = ExecutionSearchFilters {
|
||||
status: query.status,
|
||||
action_ref: query.action_ref.clone(),
|
||||
pack_name: query.pack_name.clone(),
|
||||
rule_ref: query.rule_ref.clone(),
|
||||
trigger_ref: query.trigger_ref.clone(),
|
||||
executor: query.executor,
|
||||
result_contains: query.result_contains.clone(),
|
||||
enforcement: query.enforcement,
|
||||
parent: query.parent,
|
||||
top_level_only: query.top_level_only == Some(true),
|
||||
limit: query.limit(),
|
||||
offset: query.offset(),
|
||||
};
|
||||
|
||||
// Apply additional filters in memory (could be optimized with database queries)
|
||||
let mut filtered_executions = executions;
|
||||
let result = ExecutionRepository::search(&state.db, &filters).await?;
|
||||
|
||||
if let Some(action_ref) = &query.action_ref {
|
||||
filtered_executions.retain(|e| e.action_ref == *action_ref);
|
||||
}
|
||||
|
||||
if let Some(pack_name) = &query.pack_name {
|
||||
filtered_executions.retain(|e| {
|
||||
// action_ref format is "pack.action"
|
||||
e.action_ref.starts_with(&format!("{}.", pack_name))
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(result_search) = &query.result_contains {
|
||||
let search_lower = result_search.to_lowercase();
|
||||
filtered_executions.retain(|e| {
|
||||
if let Some(result) = &e.result {
|
||||
// Convert result to JSON string and search case-insensitively
|
||||
let result_str = serde_json::to_string(result).unwrap_or_default();
|
||||
result_str.to_lowercase().contains(&search_lower)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(parent_id) = query.parent {
|
||||
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));
|
||||
}
|
||||
|
||||
// Fetch enforcements for all executions to populate rule_ref and trigger_ref
|
||||
let enforcement_ids: Vec<i64> = filtered_executions
|
||||
.iter()
|
||||
.filter_map(|e| e.enforcement)
|
||||
let paginated_executions: Vec<ExecutionSummary> = result
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(ExecutionSummary::from)
|
||||
.collect();
|
||||
|
||||
let enforcement_map: std::collections::HashMap<i64, _> = if !enforcement_ids.is_empty() {
|
||||
let enforcements = EnforcementRepository::list(&state.db).await?;
|
||||
enforcements.into_iter().map(|enf| (enf.id, enf)).collect()
|
||||
} else {
|
||||
std::collections::HashMap::new()
|
||||
};
|
||||
|
||||
// Filter by rule_ref if specified
|
||||
if let Some(rule_ref) = &query.rule_ref {
|
||||
filtered_executions.retain(|e| {
|
||||
e.enforcement
|
||||
.and_then(|enf_id| enforcement_map.get(&enf_id))
|
||||
.map(|enf| enf.rule_ref == *rule_ref)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by trigger_ref if specified
|
||||
if let Some(trigger_ref) = &query.trigger_ref {
|
||||
filtered_executions.retain(|e| {
|
||||
e.enforcement
|
||||
.and_then(|enf_id| enforcement_map.get(&enf_id))
|
||||
.map(|enf| enf.trigger_ref == *trigger_ref)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_executions.len() as u64;
|
||||
let start = query.offset() as usize;
|
||||
let end = (start + query.limit() as usize).min(filtered_executions.len());
|
||||
|
||||
// Get paginated slice and populate rule_ref/trigger_ref from enforcements
|
||||
let paginated_executions: Vec<ExecutionSummary> = filtered_executions[start..end]
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let mut summary = ExecutionSummary::from(e.clone());
|
||||
if let Some(enf_id) = e.enforcement {
|
||||
if let Some(enforcement) = enforcement_map.get(&enf_id) {
|
||||
summary.rule_ref = Some(enforcement.rule_ref.clone());
|
||||
summary.trigger_ref = Some(enforcement.trigger_ref.clone());
|
||||
}
|
||||
}
|
||||
summary
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Convert query params to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: query.page,
|
||||
page_size: query.per_page,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination_params, total);
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination_params, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -310,21 +231,23 @@ pub async fn list_executions_by_status(
|
||||
}
|
||||
};
|
||||
|
||||
// Get executions by status
|
||||
let executions = ExecutionRepository::find_by_status(&state.db, status).await?;
|
||||
// Use the search method for SQL-side filtering + pagination.
|
||||
let filters = ExecutionSearchFilters {
|
||||
status: Some(status),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = executions.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(executions.len());
|
||||
let result = ExecutionRepository::search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_executions: Vec<ExecutionSummary> = executions[start..end]
|
||||
.iter()
|
||||
.map(|e| ExecutionSummary::from(e.clone()))
|
||||
let paginated_executions: Vec<ExecutionSummary> = result
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(ExecutionSummary::from)
|
||||
.collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -350,21 +273,23 @@ pub async fn list_executions_by_enforcement(
|
||||
Path(enforcement_id): Path<i64>,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get executions by enforcement
|
||||
let executions = ExecutionRepository::find_by_enforcement(&state.db, enforcement_id).await?;
|
||||
// Use the search method for SQL-side filtering + pagination.
|
||||
let filters = ExecutionSearchFilters {
|
||||
enforcement: Some(enforcement_id),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = executions.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(executions.len());
|
||||
let result = ExecutionRepository::search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_executions: Vec<ExecutionSummary> = executions[start..end]
|
||||
.iter()
|
||||
.map(|e| ExecutionSummary::from(e.clone()))
|
||||
let paginated_executions: Vec<ExecutionSummary> = result
|
||||
.rows
|
||||
.into_iter()
|
||||
.map(ExecutionSummary::from)
|
||||
.collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_executions, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -384,34 +309,37 @@ pub async fn get_execution_stats(
|
||||
State(state): State<Arc<AppState>>,
|
||||
RequireAuth(_user): RequireAuth,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get all executions (limited by repository to 1000)
|
||||
let executions = ExecutionRepository::list(&state.db).await?;
|
||||
// Use a single SQL query with COUNT + GROUP BY instead of fetching all rows.
|
||||
let rows = sqlx::query(
|
||||
"SELECT status::text AS status, COUNT(*) AS cnt FROM execution GROUP BY status",
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
// Calculate statistics
|
||||
let total = executions.len();
|
||||
let completed = executions
|
||||
.iter()
|
||||
.filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Completed)
|
||||
.count();
|
||||
let failed = executions
|
||||
.iter()
|
||||
.filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Failed)
|
||||
.count();
|
||||
let running = executions
|
||||
.iter()
|
||||
.filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Running)
|
||||
.count();
|
||||
let pending = executions
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e.status,
|
||||
attune_common::models::enums::ExecutionStatus::Requested
|
||||
| attune_common::models::enums::ExecutionStatus::Scheduling
|
||||
| attune_common::models::enums::ExecutionStatus::Scheduled
|
||||
)
|
||||
})
|
||||
.count();
|
||||
let mut completed: i64 = 0;
|
||||
let mut failed: i64 = 0;
|
||||
let mut running: i64 = 0;
|
||||
let mut pending: i64 = 0;
|
||||
let mut cancelled: i64 = 0;
|
||||
let mut timeout: i64 = 0;
|
||||
let mut abandoned: i64 = 0;
|
||||
let mut total: i64 = 0;
|
||||
|
||||
for row in &rows {
|
||||
let status: &str = row.get("status");
|
||||
let cnt: i64 = row.get("cnt");
|
||||
total += cnt;
|
||||
match status {
|
||||
"completed" => completed = cnt,
|
||||
"failed" => failed = cnt,
|
||||
"running" => running = cnt,
|
||||
"requested" | "scheduling" | "scheduled" => pending += cnt,
|
||||
"cancelled" | "canceling" => cancelled += cnt,
|
||||
"timeout" => timeout = cnt,
|
||||
"abandoned" => abandoned = cnt,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let stats = serde_json::json!({
|
||||
"total": total,
|
||||
@@ -419,9 +347,9 @@ pub async fn get_execution_stats(
|
||||
"failed": failed,
|
||||
"running": running,
|
||||
"pending": pending,
|
||||
"cancelled": executions.iter().filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Cancelled).count(),
|
||||
"timeout": executions.iter().filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Timeout).count(),
|
||||
"abandoned": executions.iter().filter(|e| e.status == attune_common::models::enums::ExecutionStatus::Abandoned).count(),
|
||||
"cancelled": cancelled,
|
||||
"timeout": timeout,
|
||||
"abandoned": abandoned,
|
||||
});
|
||||
|
||||
let response = ApiResponse::new(stats);
|
||||
|
||||
@@ -14,8 +14,10 @@ use attune_common::{
|
||||
mq::{InquiryRespondedPayload, MessageEnvelope, MessageType},
|
||||
repositories::{
|
||||
execution::ExecutionRepository,
|
||||
inquiry::{CreateInquiryInput, InquiryRepository, UpdateInquiryInput},
|
||||
Create, Delete, FindById, List, Update,
|
||||
inquiry::{
|
||||
CreateInquiryInput, InquiryRepository, InquirySearchFilters, UpdateInquiryInput,
|
||||
},
|
||||
Create, Delete, FindById, Update,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,45 +53,30 @@ pub async fn list_inquiries(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<InquiryQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get inquiries based on filters
|
||||
let inquiries = if let Some(status) = query.status {
|
||||
// Filter by status
|
||||
InquiryRepository::find_by_status(&state.db, status).await?
|
||||
} else if let Some(execution_id) = query.execution {
|
||||
// Filter by execution
|
||||
InquiryRepository::find_by_execution(&state.db, execution_id).await?
|
||||
} else {
|
||||
// Get all inquiries
|
||||
InquiryRepository::list(&state.db).await?
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
// Filters are combinable (AND), not mutually exclusive.
|
||||
let limit = query.limit.unwrap_or(50).min(500) as u32;
|
||||
let offset = query.offset.unwrap_or(0) as u32;
|
||||
|
||||
let filters = InquirySearchFilters {
|
||||
status: query.status,
|
||||
execution: query.execution,
|
||||
assigned_to: query.assigned_to,
|
||||
limit,
|
||||
offset,
|
||||
};
|
||||
|
||||
// Apply additional filters in memory
|
||||
let mut filtered_inquiries = inquiries;
|
||||
let result = InquiryRepository::search(&state.db, &filters).await?;
|
||||
|
||||
if let Some(assigned_to) = query.assigned_to {
|
||||
filtered_inquiries.retain(|i| i.assigned_to == Some(assigned_to));
|
||||
}
|
||||
let paginated_inquiries: Vec<InquirySummary> =
|
||||
result.rows.into_iter().map(InquirySummary::from).collect();
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_inquiries.len() as u64;
|
||||
let offset = query.offset.unwrap_or(0);
|
||||
let limit = query.limit.unwrap_or(50).min(500);
|
||||
let start = offset;
|
||||
let end = (start + limit).min(filtered_inquiries.len());
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_inquiries: Vec<InquirySummary> = filtered_inquiries[start..end]
|
||||
.iter()
|
||||
.map(|inquiry| InquirySummary::from(inquiry.clone()))
|
||||
.collect();
|
||||
|
||||
// Convert to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: (offset / limit.max(1)) as u32 + 1,
|
||||
page_size: limit as u32,
|
||||
page: (offset / limit.max(1)) + 1,
|
||||
page_size: limit,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination_params, total);
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination_params, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -161,20 +148,21 @@ pub async fn list_inquiries_by_status(
|
||||
}
|
||||
};
|
||||
|
||||
let inquiries = InquiryRepository::find_by_status(&state.db, status).await?;
|
||||
// Use the search method for SQL-side filtering + pagination.
|
||||
let filters = InquirySearchFilters {
|
||||
status: Some(status),
|
||||
execution: None,
|
||||
assigned_to: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = inquiries.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(inquiries.len());
|
||||
let result = InquiryRepository::search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_inquiries: Vec<InquirySummary> = inquiries[start..end]
|
||||
.iter()
|
||||
.map(|inquiry| InquirySummary::from(inquiry.clone()))
|
||||
.collect();
|
||||
let paginated_inquiries: Vec<InquirySummary> =
|
||||
result.rows.into_iter().map(InquirySummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -209,20 +197,21 @@ pub async fn list_inquiries_by_execution(
|
||||
ApiError::NotFound(format!("Execution with ID {} not found", execution_id))
|
||||
})?;
|
||||
|
||||
let inquiries = InquiryRepository::find_by_execution(&state.db, execution_id).await?;
|
||||
// Use the search method for SQL-side filtering + pagination.
|
||||
let filters = InquirySearchFilters {
|
||||
status: None,
|
||||
execution: Some(execution_id),
|
||||
assigned_to: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = inquiries.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(inquiries.len());
|
||||
let result = InquiryRepository::search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_inquiries: Vec<InquirySummary> = inquiries[start..end]
|
||||
.iter()
|
||||
.map(|inquiry| InquirySummary::from(inquiry.clone()))
|
||||
.collect();
|
||||
let paginated_inquiries: Vec<InquirySummary> =
|
||||
result.rows.into_iter().map(InquirySummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_inquiries, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ use validator::Validate;
|
||||
use attune_common::models::OwnerType;
|
||||
use attune_common::repositories::{
|
||||
action::ActionRepository,
|
||||
key::{CreateKeyInput, KeyRepository, UpdateKeyInput},
|
||||
key::{CreateKeyInput, KeyRepository, KeySearchFilters, UpdateKeyInput},
|
||||
pack::PackRepository,
|
||||
trigger::SensorRepository,
|
||||
Create, Delete, FindByRef, List, Update,
|
||||
Create, Delete, FindByRef, Update,
|
||||
};
|
||||
|
||||
use crate::auth::RequireAuth;
|
||||
@@ -46,40 +46,24 @@ pub async fn list_keys(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<KeyQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get keys based on filters
|
||||
let keys = if let Some(owner_type) = query.owner_type {
|
||||
// Filter by owner type
|
||||
KeyRepository::find_by_owner_type(&state.db, owner_type).await?
|
||||
} else {
|
||||
// Get all keys
|
||||
KeyRepository::list(&state.db).await?
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = KeySearchFilters {
|
||||
owner_type: query.owner_type,
|
||||
owner: query.owner.clone(),
|
||||
limit: query.limit(),
|
||||
offset: query.offset(),
|
||||
};
|
||||
|
||||
// Apply additional filters in memory
|
||||
let mut filtered_keys = keys;
|
||||
let result = KeyRepository::search(&state.db, &filters).await?;
|
||||
|
||||
if let Some(owner) = &query.owner {
|
||||
filtered_keys.retain(|k| k.owner.as_ref() == Some(owner));
|
||||
}
|
||||
let paginated_keys: Vec<KeySummary> = result.rows.into_iter().map(KeySummary::from).collect();
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_keys.len() as u64;
|
||||
let start = query.offset() as usize;
|
||||
let end = (start + query.limit() as usize).min(filtered_keys.len());
|
||||
|
||||
// Get paginated slice (values redacted in summary)
|
||||
let paginated_keys: Vec<KeySummary> = filtered_keys[start..end]
|
||||
.iter()
|
||||
.map(|key| KeySummary::from(key.clone()))
|
||||
.collect();
|
||||
|
||||
// Convert query params to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: query.page,
|
||||
page_size: query.per_page,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_keys, &pagination_params, total);
|
||||
let response = PaginatedResponse::new(paginated_keys, &pagination_params, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ use attune_common::mq::{
|
||||
use attune_common::repositories::{
|
||||
action::ActionRepository,
|
||||
pack::PackRepository,
|
||||
rule::{CreateRuleInput, RuleRepository, UpdateRuleInput},
|
||||
rule::{CreateRuleInput, RuleRepository, RuleSearchFilters, UpdateRuleInput},
|
||||
trigger::TriggerRepository,
|
||||
Create, Delete, FindByRef, List, Update,
|
||||
Create, Delete, FindByRef, Update,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -50,21 +50,21 @@ pub async fn list_rules(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get all rules
|
||||
let rules = RuleRepository::list(&state.db).await?;
|
||||
let filters = RuleSearchFilters {
|
||||
pack: None,
|
||||
action: None,
|
||||
trigger: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = rules.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(rules.len());
|
||||
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_rules: Vec<RuleSummary> = rules[start..end]
|
||||
.iter()
|
||||
.map(|r| RuleSummary::from(r.clone()))
|
||||
.collect();
|
||||
let paginated_rules: Vec<RuleSummary> =
|
||||
result.rows.into_iter().map(RuleSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -85,21 +85,21 @@ pub async fn list_enabled_rules(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get enabled rules
|
||||
let rules = RuleRepository::find_enabled(&state.db).await?;
|
||||
let filters = RuleSearchFilters {
|
||||
pack: None,
|
||||
action: None,
|
||||
trigger: None,
|
||||
enabled: Some(true),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = rules.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(rules.len());
|
||||
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_rules: Vec<RuleSummary> = rules[start..end]
|
||||
.iter()
|
||||
.map(|r| RuleSummary::from(r.clone()))
|
||||
.collect();
|
||||
let paginated_rules: Vec<RuleSummary> =
|
||||
result.rows.into_iter().map(RuleSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -130,21 +130,21 @@ pub async fn list_rules_by_pack(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
||||
|
||||
// Get rules for this pack
|
||||
let rules = RuleRepository::find_by_pack(&state.db, pack.id).await?;
|
||||
let filters = RuleSearchFilters {
|
||||
pack: Some(pack.id),
|
||||
action: None,
|
||||
trigger: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = rules.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(rules.len());
|
||||
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_rules: Vec<RuleSummary> = rules[start..end]
|
||||
.iter()
|
||||
.map(|r| RuleSummary::from(r.clone()))
|
||||
.collect();
|
||||
let paginated_rules: Vec<RuleSummary> =
|
||||
result.rows.into_iter().map(RuleSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -175,21 +175,21 @@ pub async fn list_rules_by_action(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Action '{}' not found", action_ref)))?;
|
||||
|
||||
// Get rules for this action
|
||||
let rules = RuleRepository::find_by_action(&state.db, action.id).await?;
|
||||
let filters = RuleSearchFilters {
|
||||
pack: None,
|
||||
action: Some(action.id),
|
||||
trigger: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = rules.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(rules.len());
|
||||
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_rules: Vec<RuleSummary> = rules[start..end]
|
||||
.iter()
|
||||
.map(|r| RuleSummary::from(r.clone()))
|
||||
.collect();
|
||||
let paginated_rules: Vec<RuleSummary> =
|
||||
result.rows.into_iter().map(RuleSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -220,21 +220,21 @@ pub async fn list_rules_by_trigger(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
||||
|
||||
// Get rules for this trigger
|
||||
let rules = RuleRepository::find_by_trigger(&state.db, trigger.id).await?;
|
||||
let filters = RuleSearchFilters {
|
||||
pack: None,
|
||||
action: None,
|
||||
trigger: Some(trigger.id),
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = rules.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(rules.len());
|
||||
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_rules: Vec<RuleSummary> = rules[start..end]
|
||||
.iter()
|
||||
.map(|r| RuleSummary::from(r.clone()))
|
||||
.collect();
|
||||
let paginated_rules: Vec<RuleSummary> =
|
||||
result.rows.into_iter().map(RuleSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ use attune_common::repositories::{
|
||||
pack::PackRepository,
|
||||
runtime::RuntimeRepository,
|
||||
trigger::{
|
||||
CreateSensorInput, CreateTriggerInput, SensorRepository, TriggerRepository,
|
||||
UpdateSensorInput, UpdateTriggerInput,
|
||||
CreateSensorInput, CreateTriggerInput, SensorRepository, SensorSearchFilters,
|
||||
TriggerRepository, TriggerSearchFilters, UpdateSensorInput, UpdateTriggerInput,
|
||||
},
|
||||
Create, Delete, FindByRef, List, Update,
|
||||
Create, Delete, FindByRef, Update,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -54,21 +54,19 @@ pub async fn list_triggers(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get all triggers
|
||||
let triggers = TriggerRepository::list(&state.db).await?;
|
||||
let filters = TriggerSearchFilters {
|
||||
pack: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = triggers.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(triggers.len());
|
||||
let result = TriggerRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_triggers: Vec<TriggerSummary> = triggers[start..end]
|
||||
.iter()
|
||||
.map(|t| TriggerSummary::from(t.clone()))
|
||||
.collect();
|
||||
let paginated_triggers: Vec<TriggerSummary> =
|
||||
result.rows.into_iter().map(TriggerSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -89,21 +87,19 @@ pub async fn list_enabled_triggers(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get enabled triggers
|
||||
let triggers = TriggerRepository::find_enabled(&state.db).await?;
|
||||
let filters = TriggerSearchFilters {
|
||||
pack: None,
|
||||
enabled: Some(true),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = triggers.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(triggers.len());
|
||||
let result = TriggerRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_triggers: Vec<TriggerSummary> = triggers[start..end]
|
||||
.iter()
|
||||
.map(|t| TriggerSummary::from(t.clone()))
|
||||
.collect();
|
||||
let paginated_triggers: Vec<TriggerSummary> =
|
||||
result.rows.into_iter().map(TriggerSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -134,21 +130,19 @@ pub async fn list_triggers_by_pack(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
||||
|
||||
// Get triggers for this pack
|
||||
let triggers = TriggerRepository::find_by_pack(&state.db, pack.id).await?;
|
||||
let filters = TriggerSearchFilters {
|
||||
pack: Some(pack.id),
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = triggers.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(triggers.len());
|
||||
let result = TriggerRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_triggers: Vec<TriggerSummary> = triggers[start..end]
|
||||
.iter()
|
||||
.map(|t| TriggerSummary::from(t.clone()))
|
||||
.collect();
|
||||
let paginated_triggers: Vec<TriggerSummary> =
|
||||
result.rows.into_iter().map(TriggerSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_triggers, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -438,21 +432,20 @@ pub async fn list_sensors(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get all sensors
|
||||
let sensors = SensorRepository::list(&state.db).await?;
|
||||
let filters = SensorSearchFilters {
|
||||
pack: None,
|
||||
trigger: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = sensors.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(sensors.len());
|
||||
let result = SensorRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_sensors: Vec<SensorSummary> = sensors[start..end]
|
||||
.iter()
|
||||
.map(|s| SensorSummary::from(s.clone()))
|
||||
.collect();
|
||||
let paginated_sensors: Vec<SensorSummary> =
|
||||
result.rows.into_iter().map(SensorSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -473,21 +466,20 @@ pub async fn list_enabled_sensors(
|
||||
RequireAuth(_user): RequireAuth,
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get enabled sensors
|
||||
let sensors = SensorRepository::find_enabled(&state.db).await?;
|
||||
let filters = SensorSearchFilters {
|
||||
pack: None,
|
||||
trigger: None,
|
||||
enabled: Some(true),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = sensors.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(sensors.len());
|
||||
let result = SensorRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_sensors: Vec<SensorSummary> = sensors[start..end]
|
||||
.iter()
|
||||
.map(|s| SensorSummary::from(s.clone()))
|
||||
.collect();
|
||||
let paginated_sensors: Vec<SensorSummary> =
|
||||
result.rows.into_iter().map(SensorSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -518,21 +510,20 @@ pub async fn list_sensors_by_pack(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
||||
|
||||
// Get sensors for this pack
|
||||
let sensors = SensorRepository::find_by_pack(&state.db, pack.id).await?;
|
||||
let filters = SensorSearchFilters {
|
||||
pack: Some(pack.id),
|
||||
trigger: None,
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = sensors.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(sensors.len());
|
||||
let result = SensorRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_sensors: Vec<SensorSummary> = sensors[start..end]
|
||||
.iter()
|
||||
.map(|s| SensorSummary::from(s.clone()))
|
||||
.collect();
|
||||
let paginated_sensors: Vec<SensorSummary> =
|
||||
result.rows.into_iter().map(SensorSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -563,21 +554,20 @@ pub async fn list_sensors_by_trigger(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
||||
|
||||
// Get sensors for this trigger
|
||||
let sensors = SensorRepository::find_by_trigger(&state.db, trigger.id).await?;
|
||||
let filters = SensorSearchFilters {
|
||||
pack: None,
|
||||
trigger: Some(trigger.id),
|
||||
enabled: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = sensors.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(sensors.len());
|
||||
let result = SensorRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_sensors: Vec<SensorSummary> = sensors[start..end]
|
||||
.iter()
|
||||
.map(|s| SensorSummary::from(s.clone()))
|
||||
.collect();
|
||||
let paginated_sensors: Vec<SensorSummary> =
|
||||
result.rows.into_iter().map(SensorSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_sensors, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ use attune_common::repositories::{
|
||||
pack::PackRepository,
|
||||
workflow::{
|
||||
CreateWorkflowDefinitionInput, UpdateWorkflowDefinitionInput, WorkflowDefinitionRepository,
|
||||
WorkflowSearchFilters,
|
||||
},
|
||||
Create, Delete, FindByRef, List, Update,
|
||||
Create, Delete, FindByRef, Update,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -54,64 +55,30 @@ pub async fn list_workflows(
|
||||
// Validate search params
|
||||
search_params.validate()?;
|
||||
|
||||
// Get workflows based on filters
|
||||
let mut workflows = if let Some(tags_str) = &search_params.tags {
|
||||
// Filter by tags
|
||||
let tags: Vec<&str> = tags_str.split(',').map(|s| s.trim()).collect();
|
||||
let mut results = Vec::new();
|
||||
for tag in tags {
|
||||
let mut tag_results = WorkflowDefinitionRepository::find_by_tag(&state.db, tag).await?;
|
||||
results.append(&mut tag_results);
|
||||
}
|
||||
// Remove duplicates by ID
|
||||
results.sort_by_key(|w| w.id);
|
||||
results.dedup_by_key(|w| w.id);
|
||||
results
|
||||
} else if search_params.enabled == Some(true) {
|
||||
// Filter by enabled status (only return enabled workflows)
|
||||
WorkflowDefinitionRepository::find_enabled(&state.db).await?
|
||||
} else {
|
||||
// Get all workflows
|
||||
WorkflowDefinitionRepository::list(&state.db).await?
|
||||
// Parse comma-separated tags into a Vec if provided
|
||||
let tags = search_params.tags.as_ref().map(|t| {
|
||||
t.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = WorkflowSearchFilters {
|
||||
pack: None,
|
||||
pack_ref: search_params.pack_ref.clone(),
|
||||
enabled: search_params.enabled,
|
||||
tags,
|
||||
search: search_params.search.clone(),
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Apply enabled filter if specified and not already filtered by it
|
||||
if let Some(enabled) = search_params.enabled {
|
||||
if search_params.tags.is_some() {
|
||||
// If we filtered by tags, also apply enabled filter
|
||||
workflows.retain(|w| w.enabled == enabled);
|
||||
}
|
||||
}
|
||||
let result = WorkflowDefinitionRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Apply search filter if provided
|
||||
if let Some(search_term) = &search_params.search {
|
||||
let search_lower = search_term.to_lowercase();
|
||||
workflows.retain(|w| {
|
||||
w.label.to_lowercase().contains(&search_lower)
|
||||
|| w.description
|
||||
.as_ref()
|
||||
.map(|d| d.to_lowercase().contains(&search_lower))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
}
|
||||
let paginated_workflows: Vec<WorkflowSummary> =
|
||||
result.rows.into_iter().map(WorkflowSummary::from).collect();
|
||||
|
||||
// Apply pack_ref filter if provided
|
||||
if let Some(pack_ref) = &search_params.pack_ref {
|
||||
workflows.retain(|w| w.pack_ref == *pack_ref);
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let total = workflows.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(workflows.len());
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_workflows: Vec<WorkflowSummary> = workflows[start..end]
|
||||
.iter()
|
||||
.map(|w| WorkflowSummary::from(w.clone()))
|
||||
.collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_workflows, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_workflows, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
@@ -138,25 +105,27 @@ pub async fn list_workflows_by_pack(
|
||||
Query(pagination): Query<PaginationParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verify pack exists
|
||||
let pack = PackRepository::find_by_ref(&state.db, &pack_ref)
|
||||
let _pack = PackRepository::find_by_ref(&state.db, &pack_ref)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
||||
|
||||
// Get workflows for this pack
|
||||
let workflows = WorkflowDefinitionRepository::find_by_pack(&state.db, pack.id).await?;
|
||||
// All filtering and pagination happen in a single SQL query.
|
||||
let filters = WorkflowSearchFilters {
|
||||
pack: None,
|
||||
pack_ref: Some(pack_ref),
|
||||
enabled: None,
|
||||
tags: None,
|
||||
search: None,
|
||||
limit: pagination.limit(),
|
||||
offset: pagination.offset(),
|
||||
};
|
||||
|
||||
// Calculate pagination
|
||||
let total = workflows.len() as u64;
|
||||
let start = ((pagination.page - 1) * pagination.limit()) as usize;
|
||||
let end = (start + pagination.limit() as usize).min(workflows.len());
|
||||
let result = WorkflowDefinitionRepository::list_search(&state.db, &filters).await?;
|
||||
|
||||
// Get paginated slice
|
||||
let paginated_workflows: Vec<WorkflowSummary> = workflows[start..end]
|
||||
.iter()
|
||||
.map(|w| WorkflowSummary::from(w.clone()))
|
||||
.collect();
|
||||
let paginated_workflows: Vec<WorkflowSummary> =
|
||||
result.rows.into_iter().map(WorkflowSummary::from).collect();
|
||||
|
||||
let response = PaginatedResponse::new(paginated_workflows, &pagination, total);
|
||||
let response = PaginatedResponse::new(paginated_workflows, &pagination, result.total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user