re-uploading work
This commit is contained in:
702
crates/common/src/repositories/action.rs
Normal file
702
crates/common/src/repositories/action.rs
Normal file
@@ -0,0 +1,702 @@
|
||||
//! Action and Policy repository for database operations
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Action and Policy entities.
|
||||
|
||||
use crate::models::{action::*, enums::PolicyMethod, Id, JsonSchema};
|
||||
use crate::{Error, Result};
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
/// Repository for Action operations
|
||||
pub struct ActionRepository;
|
||||
|
||||
impl Repository for ActionRepository {
|
||||
type Entity = Action;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"action"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new action
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateActionInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Option<Id>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
pub is_adhoc: bool,
|
||||
}
|
||||
|
||||
/// Input for updating an action
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateActionInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: Option<String>,
|
||||
pub runtime: Option<Id>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for ActionRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for ActionRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for ActionRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let actions = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for ActionRepository {
|
||||
type CreateInput = CreateActionInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Validate ref format
|
||||
if !input
|
||||
.r#ref
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')
|
||||
{
|
||||
return Err(Error::validation(
|
||||
"Action ref must contain only alphanumeric characters, dots, underscores, and hyphens",
|
||||
));
|
||||
}
|
||||
|
||||
// Try to insert - database will enforce uniqueness constraint
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
INSERT INTO action (ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_adhoc)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(&input.entrypoint)
|
||||
.bind(input.runtime)
|
||||
.bind(&input.param_schema)
|
||||
.bind(&input.out_schema)
|
||||
.bind(input.is_adhoc)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert unique constraint violation to AlreadyExists error
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.is_unique_violation() {
|
||||
return Error::already_exists("Action", "ref", &input.r#ref);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for ActionRepository {
|
||||
type UpdateInput = UpdateActionInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Build dynamic UPDATE query
|
||||
let mut query = QueryBuilder::new("UPDATE action SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("label = ");
|
||||
query.push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(entrypoint) = &input.entrypoint {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("entrypoint = ");
|
||||
query.push_bind(entrypoint);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(runtime) = input.runtime {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("runtime = ");
|
||||
query.push_bind(runtime);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(param_schema) = &input.param_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("param_schema = ");
|
||||
query.push_bind(param_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(out_schema) = &input.out_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("out_schema = ");
|
||||
query.push_bind(out_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
// No updates requested, fetch and return existing action
|
||||
return Self::find_by_id(executor, id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::not_found("action", "id", id.to_string()));
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ");
|
||||
query.push_bind(id);
|
||||
query.push(" RETURNING id, ref, pack, pack_ref, label, description, entrypoint, runtime, param_schema, out_schema, is_workflow, workflow_def, created, updated");
|
||||
|
||||
let action = query
|
||||
.build_query_as::<Action>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => Error::not_found("action", "id", id.to_string()),
|
||||
_ => e.into(),
|
||||
})?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for ActionRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM action WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionRepository {
|
||||
/// Find actions by pack ID
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let actions = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
/// Find actions by runtime ID
|
||||
pub async fn find_by_runtime<'e, E>(executor: E, runtime_id: Id) -> Result<Vec<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let actions = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE runtime = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(runtime_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
/// Search actions by name/label
|
||||
pub async fn search<'e, E>(executor: E, query: &str) -> Result<Vec<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let search_pattern = format!("%{}%", query.to_lowercase());
|
||||
let actions = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, 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
|
||||
"#,
|
||||
)
|
||||
.bind(&search_pattern)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
/// Find all workflow actions (actions where is_workflow = true)
|
||||
pub async fn find_workflows<'e, E>(executor: E) -> Result<Vec<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let actions = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE is_workflow = true
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
/// Find action by workflow definition ID
|
||||
pub async fn find_by_workflow_def<'e, E>(
|
||||
executor: E,
|
||||
workflow_def_id: Id,
|
||||
) -> Result<Option<Action>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
FROM action
|
||||
WHERE workflow_def = $1
|
||||
"#,
|
||||
)
|
||||
.bind(workflow_def_id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
|
||||
/// Link an action to a workflow definition
|
||||
pub async fn link_workflow_def<'e, E>(
|
||||
executor: E,
|
||||
action_id: Id,
|
||||
workflow_def_id: Id,
|
||||
) -> Result<Action>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let action = sqlx::query_as::<_, Action>(
|
||||
r#"
|
||||
UPDATE action
|
||||
SET is_workflow = true, workflow_def = $2, updated = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, param_schema, out_schema, is_workflow, workflow_def, is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
.bind(workflow_def_id)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => Error::not_found("action", "id", action_id.to_string()),
|
||||
_ => e.into(),
|
||||
})?;
|
||||
|
||||
Ok(action)
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository for Policy operations
|
||||
// ============================================================================
|
||||
// Policy Repository
|
||||
// ============================================================================
|
||||
|
||||
/// Repository for Policy operations
|
||||
pub struct PolicyRepository;
|
||||
|
||||
impl Repository for PolicyRepository {
|
||||
type Entity = Policy;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"policies"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new policy
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePolicyInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub action: Option<Id>,
|
||||
pub action_ref: Option<String>,
|
||||
pub parameters: Vec<String>,
|
||||
pub method: PolicyMethod,
|
||||
pub threshold: i32,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// Input for updating a policy
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdatePolicyInput {
|
||||
pub parameters: Option<Vec<String>>,
|
||||
pub method: Option<PolicyMethod>,
|
||||
pub threshold: Option<i32>,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for PolicyRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let policy = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
FROM policies
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for PolicyRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let policy = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
FROM policies
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for PolicyRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let policies = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
FROM policies
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(policies)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for PolicyRepository {
|
||||
type CreateInput = CreatePolicyInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Try to insert - database will enforce uniqueness constraint
|
||||
let policy = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
INSERT INTO policies (ref, pack, pack_ref, action, action_ref, parameters,
|
||||
method, threshold, name, description, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(input.action)
|
||||
.bind(&input.action_ref)
|
||||
.bind(&input.parameters)
|
||||
.bind(input.method)
|
||||
.bind(input.threshold)
|
||||
.bind(&input.name)
|
||||
.bind(&input.description)
|
||||
.bind(&input.tags)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert unique constraint violation to AlreadyExists error
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.is_unique_violation() {
|
||||
return Error::already_exists("Policy", "ref", &input.r#ref);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for PolicyRepository {
|
||||
type UpdateInput = UpdatePolicyInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let mut query = QueryBuilder::new("UPDATE policies SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(parameters) = &input.parameters {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("parameters = ");
|
||||
query.push_bind(parameters);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(method) = input.method {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("method = ");
|
||||
query.push_bind(method);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(threshold) = input.threshold {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("threshold = ");
|
||||
query.push_bind(threshold);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(name) = &input.name {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("name = ");
|
||||
query.push_bind(name);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(tags) = &input.tags {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("tags = ");
|
||||
query.push_bind(tags);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
// No updates requested, fetch and return existing policy
|
||||
return Self::get_by_id(executor, id).await;
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ");
|
||||
query.push_bind(id);
|
||||
query.push(" RETURNING id, ref, pack, pack_ref, action, action_ref, parameters, method, threshold, name, description, tags, created, updated");
|
||||
|
||||
let policy = query.build_query_as::<Policy>().fetch_one(executor).await?;
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for PolicyRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM policies WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PolicyRepository {
|
||||
/// Find policies by action ID
|
||||
pub async fn find_by_action<'e, E>(executor: E, action_id: Id) -> Result<Vec<Policy>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let policies = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
FROM policies
|
||||
WHERE action = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(policies)
|
||||
}
|
||||
|
||||
/// Find policies by tag
|
||||
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<Policy>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let policies = sqlx::query_as::<_, Policy>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, action, action_ref, parameters, method,
|
||||
threshold, name, description, tags, created, updated
|
||||
FROM policies
|
||||
WHERE $1 = ANY(tags)
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(tag)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(policies)
|
||||
}
|
||||
}
|
||||
300
crates/common/src/repositories/artifact.rs
Normal file
300
crates/common/src/repositories/artifact.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
//! Artifact repository for database operations
|
||||
|
||||
use crate::models::{
|
||||
artifact::*,
|
||||
enums::{ArtifactType, OwnerType, RetentionPolicyType},
|
||||
};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
pub struct ArtifactRepository;
|
||||
|
||||
impl Repository for ArtifactRepository {
|
||||
type Entity = Artifact;
|
||||
fn table_name() -> &'static str {
|
||||
"artifact"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateArtifactInput {
|
||||
pub r#ref: String,
|
||||
pub scope: OwnerType,
|
||||
pub owner: String,
|
||||
pub r#type: ArtifactType,
|
||||
pub retention_policy: RetentionPolicyType,
|
||||
pub retention_limit: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateArtifactInput {
|
||||
pub r#ref: Option<String>,
|
||||
pub scope: Option<OwnerType>,
|
||||
pub owner: Option<String>,
|
||||
pub r#type: Option<ArtifactType>,
|
||||
pub retention_policy: Option<RetentionPolicyType>,
|
||||
pub retention_limit: Option<i32>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for ArtifactRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for ArtifactRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE ref = $1",
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for ArtifactRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000",
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for ArtifactRepository {
|
||||
type CreateInput = CreateArtifactInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"INSERT INTO artifact (ref, scope, owner, type, retention_policy, retention_limit)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, ref, scope, owner, type, retention_policy, retention_limit, created, updated",
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.scope)
|
||||
.bind(&input.owner)
|
||||
.bind(input.r#type)
|
||||
.bind(input.retention_policy)
|
||||
.bind(input.retention_limit)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for ArtifactRepository {
|
||||
type UpdateInput = UpdateArtifactInput;
|
||||
|
||||
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 dynamically
|
||||
let mut query = QueryBuilder::new("UPDATE artifact SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(ref_value) = &input.r#ref {
|
||||
query.push("ref = ").push_bind(ref_value);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(scope) = input.scope {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("scope = ").push_bind(scope);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(owner) = &input.owner {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("owner = ").push_bind(owner);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(artifact_type) = input.r#type {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("type = ").push_bind(artifact_type);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(retention_policy) = input.retention_policy {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query
|
||||
.push("retention_policy = ")
|
||||
.push_bind(retention_policy);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(retention_limit) = input.retention_limit {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("retention_limit = ").push_bind(retention_limit);
|
||||
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 = ").push_bind(id);
|
||||
query.push(" RETURNING id, ref, scope, owner, type, retention_policy, retention_limit, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<Artifact>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for ArtifactRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM artifact WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtifactRepository {
|
||||
/// Find artifacts by scope
|
||||
pub async fn find_by_scope<'e, E>(executor: E, scope: OwnerType) -> Result<Vec<Artifact>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE scope = $1
|
||||
ORDER BY created DESC",
|
||||
)
|
||||
.bind(scope)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find artifacts by owner
|
||||
pub async fn find_by_owner<'e, E>(executor: E, owner: &str) -> Result<Vec<Artifact>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE owner = $1
|
||||
ORDER BY created DESC",
|
||||
)
|
||||
.bind(owner)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find artifacts by type
|
||||
pub async fn find_by_type<'e, E>(
|
||||
executor: E,
|
||||
artifact_type: ArtifactType,
|
||||
) -> Result<Vec<Artifact>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE type = $1
|
||||
ORDER BY created DESC",
|
||||
)
|
||||
.bind(artifact_type)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find artifacts by scope and owner (common query pattern)
|
||||
pub async fn find_by_scope_and_owner<'e, E>(
|
||||
executor: E,
|
||||
scope: OwnerType,
|
||||
owner: &str,
|
||||
) -> Result<Vec<Artifact>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE scope = $1 AND owner = $2
|
||||
ORDER BY created DESC",
|
||||
)
|
||||
.bind(scope)
|
||||
.bind(owner)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find artifacts by retention policy
|
||||
pub async fn find_by_retention_policy<'e, E>(
|
||||
executor: E,
|
||||
retention_policy: RetentionPolicyType,
|
||||
) -> Result<Vec<Artifact>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Artifact>(
|
||||
"SELECT id, ref, scope, owner, type, retention_policy, retention_limit, created, updated
|
||||
FROM artifact
|
||||
WHERE retention_policy = $1
|
||||
ORDER BY created DESC",
|
||||
)
|
||||
.bind(retention_policy)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
465
crates/common/src/repositories/event.rs
Normal file
465
crates/common/src/repositories/event.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
//! Event and Enforcement repository for database operations
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Event and Enforcement entities.
|
||||
|
||||
use crate::models::{
|
||||
enums::{EnforcementCondition, EnforcementStatus},
|
||||
event::*,
|
||||
Id, JsonDict,
|
||||
};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
/// Repository for Event operations
|
||||
pub struct EventRepository;
|
||||
|
||||
impl Repository for EventRepository {
|
||||
type Entity = Event;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"event"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new event
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateEventInput {
|
||||
pub trigger: Option<Id>,
|
||||
pub trigger_ref: String,
|
||||
pub config: Option<JsonDict>,
|
||||
pub payload: Option<JsonDict>,
|
||||
pub source: Option<Id>,
|
||||
pub source_ref: Option<String>,
|
||||
pub rule: Option<Id>,
|
||||
pub rule_ref: Option<String>,
|
||||
}
|
||||
|
||||
/// 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>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let event = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
FROM event
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for EventRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
FROM event
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for EventRepository {
|
||||
type CreateInput = CreateEventInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let event = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
INSERT INTO event (trigger, trigger_ref, config, payload, source, source_ref, rule, rule_ref)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(input.trigger)
|
||||
.bind(&input.trigger_ref)
|
||||
.bind(&input.config)
|
||||
.bind(&input.payload)
|
||||
.bind(input.source)
|
||||
.bind(&input.source_ref)
|
||||
.bind(input.rule)
|
||||
.bind(&input.rule_ref)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl 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>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM event WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventRepository {
|
||||
/// Find events by trigger ID
|
||||
pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result<Vec<Event>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
FROM event
|
||||
WHERE trigger = $1
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Find events by trigger ref
|
||||
pub async fn find_by_trigger_ref<'e, E>(executor: E, trigger_ref: &str) -> Result<Vec<Event>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let events = sqlx::query_as::<_, Event>(
|
||||
r#"
|
||||
SELECT id, trigger, trigger_ref, config, payload, source, source_ref,
|
||||
rule, rule_ref, created, updated
|
||||
FROM event
|
||||
WHERE trigger_ref = $1
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_ref)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Enforcement Repository
|
||||
// ============================================================================
|
||||
|
||||
/// Repository for Enforcement operations
|
||||
pub struct EnforcementRepository;
|
||||
|
||||
impl Repository for EnforcementRepository {
|
||||
type Entity = Enforcement;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"enforcement"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new enforcement
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateEnforcementInput {
|
||||
pub rule: Option<Id>,
|
||||
pub rule_ref: String,
|
||||
pub trigger_ref: String,
|
||||
pub config: Option<JsonDict>,
|
||||
pub event: Option<Id>,
|
||||
pub status: EnforcementStatus,
|
||||
pub payload: JsonDict,
|
||||
pub condition: EnforcementCondition,
|
||||
pub conditions: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Input for updating an enforcement
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateEnforcementInput {
|
||||
pub status: Option<EnforcementStatus>,
|
||||
pub payload: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for EnforcementRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcement = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
FROM enforcement
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcement)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for EnforcementRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
FROM enforcement
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcements)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for EnforcementRepository {
|
||||
type CreateInput = CreateEnforcementInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcement = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
INSERT INTO enforcement (rule, rule_ref, trigger_ref, config, event, status,
|
||||
payload, condition, conditions)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(input.rule)
|
||||
.bind(&input.rule_ref)
|
||||
.bind(&input.trigger_ref)
|
||||
.bind(&input.config)
|
||||
.bind(input.event)
|
||||
.bind(input.status)
|
||||
.bind(&input.payload)
|
||||
.bind(input.condition)
|
||||
.bind(&input.conditions)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcement)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for EnforcementRepository {
|
||||
type UpdateInput = UpdateEnforcementInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Build update query
|
||||
|
||||
let mut query = QueryBuilder::new("UPDATE enforcement SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(status) = input.status {
|
||||
query.push("status = ");
|
||||
query.push_bind(status);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(payload) = &input.payload {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("payload = ");
|
||||
query.push_bind(payload);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !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, rule, rule_ref, trigger_ref, config, event, status, payload, condition, conditions, created, updated");
|
||||
|
||||
let enforcement = query
|
||||
.build_query_as::<Enforcement>()
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcement)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for EnforcementRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM enforcement WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl EnforcementRepository {
|
||||
/// Find enforcements by rule ID
|
||||
pub async fn find_by_rule<'e, E>(executor: E, rule_id: Id) -> Result<Vec<Enforcement>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
FROM enforcement
|
||||
WHERE rule = $1
|
||||
ORDER BY created DESC
|
||||
"#,
|
||||
)
|
||||
.bind(rule_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcements)
|
||||
}
|
||||
|
||||
/// Find enforcements by status
|
||||
pub async fn find_by_status<'e, E>(
|
||||
executor: E,
|
||||
status: EnforcementStatus,
|
||||
) -> Result<Vec<Enforcement>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
FROM enforcement
|
||||
WHERE status = $1
|
||||
ORDER BY created DESC
|
||||
"#,
|
||||
)
|
||||
.bind(status)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcements)
|
||||
}
|
||||
|
||||
/// Find enforcements by event ID
|
||||
pub async fn find_by_event<'e, E>(executor: E, event_id: Id) -> Result<Vec<Enforcement>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let enforcements = sqlx::query_as::<_, Enforcement>(
|
||||
r#"
|
||||
SELECT id, rule, rule_ref, trigger_ref, config, event, status, payload,
|
||||
condition, conditions, created, updated
|
||||
FROM enforcement
|
||||
WHERE event = $1
|
||||
ORDER BY created DESC
|
||||
"#,
|
||||
)
|
||||
.bind(event_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(enforcements)
|
||||
}
|
||||
}
|
||||
180
crates/common/src/repositories/execution.rs
Normal file
180
crates/common/src/repositories/execution.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Execution repository for database operations
|
||||
|
||||
use crate::models::{enums::ExecutionStatus, execution::*, Id, JsonDict};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
pub struct ExecutionRepository;
|
||||
|
||||
impl Repository for ExecutionRepository {
|
||||
type Entity = Execution;
|
||||
fn table_name() -> &'static str {
|
||||
"executions"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateExecutionInput {
|
||||
pub action: Option<Id>,
|
||||
pub action_ref: String,
|
||||
pub config: Option<JsonDict>,
|
||||
pub parent: Option<Id>,
|
||||
pub enforcement: Option<Id>,
|
||||
pub executor: Option<Id>,
|
||||
pub status: ExecutionStatus,
|
||||
pub result: Option<JsonDict>,
|
||||
pub workflow_task: Option<WorkflowTaskMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateExecutionInput {
|
||||
pub status: Option<ExecutionStatus>,
|
||||
pub result: Option<JsonDict>,
|
||||
pub executor: Option<Id>,
|
||||
pub workflow_task: Option<WorkflowTaskMetadata>,
|
||||
}
|
||||
|
||||
impl From<Execution> for UpdateExecutionInput {
|
||||
fn from(execution: Execution) -> Self {
|
||||
Self {
|
||||
status: Some(execution.status),
|
||||
result: execution.result,
|
||||
executor: execution.executor,
|
||||
workflow_task: execution.workflow_task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for ExecutionRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, parent, enforcement, executor, status, result, workflow_task, created, updated FROM execution WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for ExecutionRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, 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)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for ExecutionRepository {
|
||||
type CreateInput = CreateExecutionInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"INSERT INTO execution (action, action_ref, config, parent, enforcement, executor, status, result, workflow_task) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, action, action_ref, config, parent, enforcement, executor, status, result, workflow_task, created, updated"
|
||||
).bind(input.action).bind(&input.action_ref).bind(&input.config).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)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for ExecutionRepository {
|
||||
type UpdateInput = UpdateExecutionInput;
|
||||
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 execution SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(status) = input.status {
|
||||
query.push("status = ").push_bind(status);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(result) = &input.result {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("result = ").push_bind(result);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(executor_id) = input.executor {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("executor = ").push_bind(executor_id);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(workflow_task) = &input.workflow_task {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query
|
||||
.push("workflow_task = ")
|
||||
.push_bind(sqlx::types::Json(workflow_task));
|
||||
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 = ").push_bind(id);
|
||||
query.push(" RETURNING id, action, action_ref, config, parent, enforcement, executor, status, result, workflow_task, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<Execution>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for ExecutionRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM execution WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecutionRepository {
|
||||
pub async fn find_by_status<'e, E>(
|
||||
executor: E,
|
||||
status: ExecutionStatus,
|
||||
) -> Result<Vec<Execution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, 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)
|
||||
}
|
||||
|
||||
pub async fn find_by_enforcement<'e, E>(
|
||||
executor: E,
|
||||
enforcement_id: Id,
|
||||
) -> Result<Vec<Execution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Execution>(
|
||||
"SELECT id, action, action_ref, config, 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)
|
||||
}
|
||||
}
|
||||
377
crates/common/src/repositories/identity.rs
Normal file
377
crates/common/src/repositories/identity.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
//! Identity and permission repository for database operations
|
||||
|
||||
use crate::models::{identity::*, Id, JsonDict};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
pub struct IdentityRepository;
|
||||
|
||||
impl Repository for IdentityRepository {
|
||||
type Entity = Identity;
|
||||
fn table_name() -> &'static str {
|
||||
"identities"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateIdentityInput {
|
||||
pub login: String,
|
||||
pub display_name: Option<String>,
|
||||
pub password_hash: Option<String>,
|
||||
pub attributes: JsonDict,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateIdentityInput {
|
||||
pub display_name: Option<String>,
|
||||
pub password_hash: Option<String>,
|
||||
pub attributes: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for IdentityRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for IdentityRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity ORDER BY login ASC"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for IdentityRepository {
|
||||
type CreateInput = CreateIdentityInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"INSERT INTO identity (login, display_name, password_hash, attributes) VALUES ($1, $2, $3, $4) RETURNING id, login, display_name, password_hash, attributes, created, updated"
|
||||
)
|
||||
.bind(&input.login)
|
||||
.bind(&input.display_name)
|
||||
.bind(&input.password_hash)
|
||||
.bind(&input.attributes)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert unique constraint violation to AlreadyExists error
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.is_unique_violation() {
|
||||
return crate::Error::already_exists("Identity", "login", &input.login);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for IdentityRepository {
|
||||
type UpdateInput = UpdateIdentityInput;
|
||||
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 identity SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(display_name) = &input.display_name {
|
||||
query.push("display_name = ").push_bind(display_name);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(password_hash) = &input.password_hash {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("password_hash = ").push_bind(password_hash);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(attributes) = &input.attributes {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("attributes = ").push_bind(attributes);
|
||||
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 = ").push_bind(id);
|
||||
query.push(
|
||||
" RETURNING id, login, display_name, password_hash, attributes, created, updated",
|
||||
);
|
||||
|
||||
query
|
||||
.build_query_as::<Identity>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert RowNotFound to NotFound error
|
||||
if matches!(e, sqlx::Error::RowNotFound) {
|
||||
return crate::Error::not_found("identity", "id", &id.to_string());
|
||||
}
|
||||
e.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for IdentityRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM identity WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityRepository {
|
||||
pub async fn find_by_login<'e, E>(executor: E, login: &str) -> Result<Option<Identity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Identity>(
|
||||
"SELECT id, login, display_name, password_hash, attributes, created, updated FROM identity WHERE login = $1"
|
||||
).bind(login).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
// Permission Set Repository
|
||||
pub struct PermissionSetRepository;
|
||||
|
||||
impl Repository for PermissionSetRepository {
|
||||
type Entity = PermissionSet;
|
||||
fn table_name() -> &'static str {
|
||||
"permission_set"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePermissionSetInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub grants: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdatePermissionSetInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub grants: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for PermissionSetRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, grants, created, updated FROM permission_set WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for PermissionSetRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, grants, created, updated FROM permission_set ORDER BY ref ASC"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for PermissionSetRepository {
|
||||
type CreateInput = CreatePermissionSetInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionSet>(
|
||||
"INSERT INTO permission_set (ref, pack, pack_ref, label, description, grants) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, ref, pack, pack_ref, label, description, grants, created, updated"
|
||||
).bind(&input.r#ref).bind(input.pack).bind(&input.pack_ref).bind(&input.label).bind(&input.description).bind(&input.grants).fetch_one(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for PermissionSetRepository {
|
||||
type UpdateInput = UpdatePermissionSetInput;
|
||||
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 permission_set SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
query.push("label = ").push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ").push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(grants) = &input.grants {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("grants = ").push_bind(grants);
|
||||
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 = ").push_bind(id);
|
||||
query.push(
|
||||
" RETURNING id, ref, pack, pack_ref, label, description, grants, created, updated",
|
||||
);
|
||||
|
||||
query
|
||||
.build_query_as::<PermissionSet>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for PermissionSetRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM permission_set WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Permission Assignment Repository
|
||||
pub struct PermissionAssignmentRepository;
|
||||
|
||||
impl Repository for PermissionAssignmentRepository {
|
||||
type Entity = PermissionAssignment;
|
||||
fn table_name() -> &'static str {
|
||||
"permission_assignment"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePermissionAssignmentInput {
|
||||
pub identity: Id,
|
||||
pub permset: Id,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for PermissionAssignmentRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionAssignment>(
|
||||
"SELECT id, identity, permset, created FROM permission_assignment WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for PermissionAssignmentRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionAssignment>(
|
||||
"SELECT id, identity, permset, created FROM permission_assignment ORDER BY created DESC"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for PermissionAssignmentRepository {
|
||||
type CreateInput = CreatePermissionAssignmentInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionAssignment>(
|
||||
"INSERT INTO permission_assignment (identity, permset) VALUES ($1, $2) RETURNING id, identity, permset, created"
|
||||
).bind(input.identity).bind(input.permset).fetch_one(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for PermissionAssignmentRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM permission_assignment WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionAssignmentRepository {
|
||||
pub async fn find_by_identity<'e, E>(
|
||||
executor: E,
|
||||
identity_id: Id,
|
||||
) -> Result<Vec<PermissionAssignment>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, PermissionAssignment>(
|
||||
"SELECT id, identity, permset, created FROM permission_assignment WHERE identity = $1",
|
||||
)
|
||||
.bind(identity_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
160
crates/common/src/repositories/inquiry.rs
Normal file
160
crates/common/src/repositories/inquiry.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
//! Inquiry repository for database operations
|
||||
|
||||
use crate::models::{enums::InquiryStatus, inquiry::*, Id, JsonDict, JsonSchema};
|
||||
use crate::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
pub struct InquiryRepository;
|
||||
|
||||
impl Repository for InquiryRepository {
|
||||
type Entity = Inquiry;
|
||||
fn table_name() -> &'static str {
|
||||
"inquiry"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateInquiryInput {
|
||||
pub execution: Id,
|
||||
pub prompt: String,
|
||||
pub response_schema: Option<JsonSchema>,
|
||||
pub assigned_to: Option<Id>,
|
||||
pub status: InquiryStatus,
|
||||
pub response: Option<JsonDict>,
|
||||
pub timeout_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateInquiryInput {
|
||||
pub status: Option<InquiryStatus>,
|
||||
pub response: Option<JsonDict>,
|
||||
pub responded_at: Option<DateTime<Utc>>,
|
||||
pub assigned_to: Option<Id>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for InquiryRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Inquiry>(
|
||||
"SELECT id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated FROM inquiry WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for InquiryRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Inquiry>(
|
||||
"SELECT id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated FROM inquiry ORDER BY created DESC LIMIT 1000"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for InquiryRepository {
|
||||
type CreateInput = CreateInquiryInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Inquiry>(
|
||||
"INSERT INTO inquiry (execution, prompt, response_schema, assigned_to, status, response, timeout_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated"
|
||||
).bind(input.execution).bind(&input.prompt).bind(&input.response_schema).bind(input.assigned_to).bind(input.status).bind(&input.response).bind(input.timeout_at).fetch_one(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for InquiryRepository {
|
||||
type UpdateInput = UpdateInquiryInput;
|
||||
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 inquiry SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(status) = input.status {
|
||||
query.push("status = ").push_bind(status);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(response) = &input.response {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("response = ").push_bind(response);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(responded_at) = input.responded_at {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("responded_at = ").push_bind(responded_at);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(assigned_to) = input.assigned_to {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("assigned_to = ").push_bind(assigned_to);
|
||||
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 = ").push_bind(id);
|
||||
query.push(" RETURNING id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<Inquiry>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for InquiryRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM inquiry WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl InquiryRepository {
|
||||
pub async fn find_by_status<'e, E>(executor: E, status: InquiryStatus) -> Result<Vec<Inquiry>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Inquiry>(
|
||||
"SELECT id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated FROM inquiry WHERE status = $1 ORDER BY created DESC"
|
||||
).bind(status).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_by_execution<'e, E>(executor: E, execution_id: Id) -> Result<Vec<Inquiry>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Inquiry>(
|
||||
"SELECT id, execution, prompt, response_schema, assigned_to, status, response, timeout_at, responded_at, created, updated FROM inquiry WHERE execution = $1 ORDER BY created DESC"
|
||||
).bind(execution_id).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
168
crates/common/src/repositories/key.rs
Normal file
168
crates/common/src/repositories/key.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Key/Secret repository for database operations
|
||||
|
||||
use crate::models::{key::*, Id, OwnerType};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
pub struct KeyRepository;
|
||||
|
||||
impl Repository for KeyRepository {
|
||||
type Entity = Key;
|
||||
fn table_name() -> &'static str {
|
||||
"key"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateKeyInput {
|
||||
pub r#ref: String,
|
||||
pub owner_type: OwnerType,
|
||||
pub owner: Option<String>,
|
||||
pub owner_identity: Option<Id>,
|
||||
pub owner_pack: Option<Id>,
|
||||
pub owner_pack_ref: Option<String>,
|
||||
pub owner_action: Option<Id>,
|
||||
pub owner_action_ref: Option<String>,
|
||||
pub owner_sensor: Option<Id>,
|
||||
pub owner_sensor_ref: Option<String>,
|
||||
pub name: String,
|
||||
pub encrypted: bool,
|
||||
pub encryption_key_hash: Option<String>,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateKeyInput {
|
||||
pub name: Option<String>,
|
||||
pub value: Option<String>,
|
||||
pub encrypted: Option<bool>,
|
||||
pub encryption_key_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for KeyRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Key>(
|
||||
"SELECT id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated FROM key WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for KeyRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Key>(
|
||||
"SELECT id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated FROM key ORDER BY ref ASC"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for KeyRepository {
|
||||
type CreateInput = CreateKeyInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Key>(
|
||||
"INSERT INTO key (ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated"
|
||||
).bind(&input.r#ref).bind(input.owner_type).bind(&input.owner).bind(input.owner_identity).bind(input.owner_pack).bind(&input.owner_pack_ref).bind(input.owner_action).bind(&input.owner_action_ref).bind(input.owner_sensor).bind(&input.owner_sensor_ref).bind(&input.name).bind(input.encrypted).bind(&input.encryption_key_hash).bind(&input.value).fetch_one(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for KeyRepository {
|
||||
type UpdateInput = UpdateKeyInput;
|
||||
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 key SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(name) = &input.name {
|
||||
query.push("name = ").push_bind(name);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(value) = &input.value {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("value = ").push_bind(value);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(encrypted) = input.encrypted {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("encrypted = ").push_bind(encrypted);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(encryption_key_hash) = &input.encryption_key_hash {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query
|
||||
.push("encryption_key_hash = ")
|
||||
.push_bind(encryption_key_hash);
|
||||
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 = ").push_bind(id);
|
||||
query.push(" RETURNING id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<Key>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for KeyRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM key WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyRepository {
|
||||
pub async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Key>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Key>(
|
||||
"SELECT id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated FROM key WHERE ref = $1"
|
||||
).bind(ref_str).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_by_owner_type<'e, E>(executor: E, owner_type: OwnerType) -> Result<Vec<Key>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Key>(
|
||||
"SELECT id, ref, owner_type, owner, owner_identity, owner_pack, owner_pack_ref, owner_action, owner_action_ref, owner_sensor, owner_sensor_ref, name, encrypted, encryption_key_hash, value, created, updated FROM key WHERE owner_type = $1 ORDER BY ref ASC"
|
||||
).bind(owner_type).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
306
crates/common/src/repositories/mod.rs
Normal file
306
crates/common/src/repositories/mod.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Repository layer for database operations
|
||||
//!
|
||||
//! This module provides the repository pattern for all database entities in Attune.
|
||||
//! Repositories abstract database operations and provide a clean interface for CRUD
|
||||
//! operations and queries.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! - Each entity has its own repository module (e.g., `pack`, `action`, `trigger`)
|
||||
//! - Repositories use SQLx for database operations
|
||||
//! - Transaction support is provided through SQLx's transaction types
|
||||
//! - All operations return `Result<T, Error>` for consistent error handling
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use attune_common::repositories::{PackRepository, FindByRef};
|
||||
//! use attune_common::db::Database;
|
||||
//!
|
||||
//! async fn example(db: &Database) -> attune_common::Result<()> {
|
||||
//! if let Some(pack) = PackRepository::find_by_ref(db.pool(), "core").await? {
|
||||
//! println!("Found pack: {}", pack.label);
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use sqlx::{Executor, Postgres, Transaction};
|
||||
|
||||
pub mod action;
|
||||
pub mod artifact;
|
||||
pub mod event;
|
||||
pub mod execution;
|
||||
pub mod identity;
|
||||
pub mod inquiry;
|
||||
pub mod key;
|
||||
pub mod notification;
|
||||
pub mod pack;
|
||||
pub mod pack_installation;
|
||||
pub mod pack_test;
|
||||
pub mod queue_stats;
|
||||
pub mod rule;
|
||||
pub mod runtime;
|
||||
pub mod trigger;
|
||||
pub mod workflow;
|
||||
|
||||
// Re-export repository types
|
||||
pub use action::{ActionRepository, PolicyRepository};
|
||||
pub use artifact::ArtifactRepository;
|
||||
pub use event::{EnforcementRepository, EventRepository};
|
||||
pub use execution::ExecutionRepository;
|
||||
pub use identity::{IdentityRepository, PermissionAssignmentRepository, PermissionSetRepository};
|
||||
pub use inquiry::InquiryRepository;
|
||||
pub use key::KeyRepository;
|
||||
pub use notification::NotificationRepository;
|
||||
pub use pack::PackRepository;
|
||||
pub use pack_installation::PackInstallationRepository;
|
||||
pub use pack_test::PackTestRepository;
|
||||
pub use queue_stats::QueueStatsRepository;
|
||||
pub use rule::RuleRepository;
|
||||
pub use runtime::{RuntimeRepository, WorkerRepository};
|
||||
pub use trigger::{SensorRepository, TriggerRepository};
|
||||
pub use workflow::{WorkflowDefinitionRepository, WorkflowExecutionRepository};
|
||||
|
||||
/// Type alias for database connection/transaction
|
||||
pub type DbConnection<'c> = &'c mut Transaction<'c, Postgres>;
|
||||
|
||||
/// Base repository trait providing common functionality
|
||||
///
|
||||
/// This trait is not meant to be used directly, but serves as a foundation
|
||||
/// for specific repository implementations.
|
||||
pub trait Repository {
|
||||
/// The entity type this repository manages
|
||||
type Entity;
|
||||
|
||||
/// Get the name of the table for this repository
|
||||
fn table_name() -> &'static str;
|
||||
}
|
||||
|
||||
/// Trait for repositories that support finding by ID
|
||||
#[async_trait::async_trait]
|
||||
pub trait FindById: Repository {
|
||||
/// Find an entity by its ID
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `id` - The ID to search for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Some(entity))` if found
|
||||
/// * `Ok(None)` if not found
|
||||
/// * `Err(error)` on database error
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> crate::Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
|
||||
/// Get an entity by its ID, returning an error if not found
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `id` - The ID to search for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(entity)` if found
|
||||
/// * `Err(NotFound)` if not found
|
||||
/// * `Err(error)` on database error
|
||||
async fn get_by_id<'e, E>(executor: E, id: i64) -> crate::Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
Self::find_by_id(executor, id)
|
||||
.await?
|
||||
.ok_or_else(|| crate::Error::not_found(Self::table_name(), "id", id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for repositories that support finding by reference
|
||||
#[async_trait::async_trait]
|
||||
pub trait FindByRef: Repository {
|
||||
/// Find an entity by its reference string
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `ref_str` - The reference string to search for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Some(entity))` if found
|
||||
/// * `Ok(None)` if not found
|
||||
/// * `Err(error)` on database error
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> crate::Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
|
||||
/// Get an entity by its reference, returning an error if not found
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `ref_str` - The reference string to search for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(entity)` if found
|
||||
/// * `Err(NotFound)` if not found
|
||||
/// * `Err(error)` on database error
|
||||
async fn get_by_ref<'e, E>(executor: E, ref_str: &str) -> crate::Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
Self::find_by_ref(executor, ref_str)
|
||||
.await?
|
||||
.ok_or_else(|| crate::Error::not_found(Self::table_name(), "ref", ref_str))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for repositories that support listing all entities
|
||||
#[async_trait::async_trait]
|
||||
pub trait List: Repository {
|
||||
/// List all entities
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<entity>)` - List of all entities
|
||||
/// * `Err(error)` on database error
|
||||
async fn list<'e, E>(executor: E) -> crate::Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
}
|
||||
|
||||
/// Trait for repositories that support creating entities
|
||||
#[async_trait::async_trait]
|
||||
pub trait Create: Repository {
|
||||
/// Input type for creating a new entity
|
||||
type CreateInput;
|
||||
|
||||
/// Create a new entity
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `input` - The data for creating the entity
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(entity)` - The created entity
|
||||
/// * `Err(error)` on database error or validation failure
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> crate::Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
}
|
||||
|
||||
/// Trait for repositories that support updating entities
|
||||
#[async_trait::async_trait]
|
||||
pub trait Update: Repository {
|
||||
/// Input type for updating an entity
|
||||
type UpdateInput;
|
||||
|
||||
/// Update an existing entity by ID
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `id` - The ID of the entity to update
|
||||
/// * `input` - The data for updating the entity
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(entity)` - The updated entity
|
||||
/// * `Err(NotFound)` if the entity doesn't exist
|
||||
/// * `Err(error)` on database error or validation failure
|
||||
async fn update<'e, E>(
|
||||
executor: E,
|
||||
id: i64,
|
||||
input: Self::UpdateInput,
|
||||
) -> crate::Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
}
|
||||
|
||||
/// Trait for repositories that support deleting entities
|
||||
#[async_trait::async_trait]
|
||||
pub trait Delete: Repository {
|
||||
/// Delete an entity by ID
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `executor` - Database executor (pool or transaction)
|
||||
/// * `id` - The ID of the entity to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(true)` if the entity was deleted
|
||||
/// * `Ok(false)` if the entity didn't exist
|
||||
/// * `Err(error)` on database error
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> crate::Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e;
|
||||
}
|
||||
|
||||
/// Helper struct for pagination parameters
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Pagination {
|
||||
/// Page number (0-based)
|
||||
pub page: i64,
|
||||
/// Number of items per page
|
||||
pub per_page: i64,
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
/// Create a new Pagination instance
|
||||
pub fn new(page: i64, per_page: i64) -> Self {
|
||||
Self { page, per_page }
|
||||
}
|
||||
|
||||
/// Calculate the OFFSET for SQL queries
|
||||
pub fn offset(&self) -> i64 {
|
||||
self.page * self.per_page
|
||||
}
|
||||
|
||||
/// Get the LIMIT for SQL queries
|
||||
pub fn limit(&self) -> i64 {
|
||||
self.per_page
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Pagination {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page: 0,
|
||||
per_page: 50,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pagination() {
|
||||
let p = Pagination::new(0, 10);
|
||||
assert_eq!(p.offset(), 0);
|
||||
assert_eq!(p.limit(), 10);
|
||||
|
||||
let p = Pagination::new(2, 10);
|
||||
assert_eq!(p.offset(), 20);
|
||||
assert_eq!(p.limit(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pagination_default() {
|
||||
let p = Pagination::default();
|
||||
assert_eq!(p.page, 0);
|
||||
assert_eq!(p.per_page, 50);
|
||||
}
|
||||
}
|
||||
145
crates/common/src/repositories/notification.rs
Normal file
145
crates/common/src/repositories/notification.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
//! Notification repository for database operations
|
||||
|
||||
use crate::models::{enums::NotificationState, notification::*, JsonDict};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, List, Repository, Update};
|
||||
|
||||
pub struct NotificationRepository;
|
||||
|
||||
impl Repository for NotificationRepository {
|
||||
type Entity = Notification;
|
||||
fn table_name() -> &'static str {
|
||||
"notification"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateNotificationInput {
|
||||
pub channel: String,
|
||||
pub entity_type: String,
|
||||
pub entity: String,
|
||||
pub activity: String,
|
||||
pub state: NotificationState,
|
||||
pub content: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateNotificationInput {
|
||||
pub state: Option<NotificationState>,
|
||||
pub content: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for NotificationRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Notification>(
|
||||
"SELECT id, channel, entity_type, entity, activity, state, content, created, updated FROM notification WHERE id = $1"
|
||||
).bind(id).fetch_optional(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for NotificationRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Notification>(
|
||||
"SELECT id, channel, entity_type, entity, activity, state, content, created, updated FROM notification ORDER BY created DESC LIMIT 1000"
|
||||
).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for NotificationRepository {
|
||||
type CreateInput = CreateNotificationInput;
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Notification>(
|
||||
"INSERT INTO notification (channel, entity_type, entity, activity, state, content) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, channel, entity_type, entity, activity, state, content, created, updated"
|
||||
).bind(&input.channel).bind(&input.entity_type).bind(&input.entity).bind(&input.activity).bind(input.state).bind(&input.content).fetch_one(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for NotificationRepository {
|
||||
type UpdateInput = UpdateNotificationInput;
|
||||
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 notification SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(state) = input.state {
|
||||
query.push("state = ").push_bind(state);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(content) = &input.content {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("content = ").push_bind(content);
|
||||
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 = ").push_bind(id);
|
||||
query.push(" RETURNING id, channel, entity_type, entity, activity, state, content, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<Notification>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for NotificationRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM notification WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationRepository {
|
||||
pub async fn find_by_state<'e, E>(
|
||||
executor: E,
|
||||
state: NotificationState,
|
||||
) -> Result<Vec<Notification>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Notification>(
|
||||
"SELECT id, channel, entity_type, entity, activity, state, content, created, updated FROM notification WHERE state = $1 ORDER BY created DESC"
|
||||
).bind(state).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn find_by_channel<'e, E>(executor: E, channel: &str) -> Result<Vec<Notification>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, Notification>(
|
||||
"SELECT id, channel, entity_type, entity, activity, state, content, created, updated FROM notification WHERE channel = $1 ORDER BY created DESC"
|
||||
).bind(channel).fetch_all(executor).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
447
crates/common/src/repositories/pack.rs
Normal file
447
crates/common/src/repositories/pack.rs
Normal file
@@ -0,0 +1,447 @@
|
||||
//! Pack repository for database operations on packs
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Pack entities.
|
||||
|
||||
use crate::models::{pack::Pack, JsonDict, JsonSchema};
|
||||
use crate::{Error, Result};
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Pagination, Repository, Update};
|
||||
|
||||
/// Repository for Pack operations
|
||||
pub struct PackRepository;
|
||||
|
||||
impl Repository for PackRepository {
|
||||
type Entity = Pack;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"pack"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new pack
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreatePackInput {
|
||||
pub r#ref: String,
|
||||
pub label: String,
|
||||
pub description: Option<String>,
|
||||
pub version: String,
|
||||
pub conf_schema: JsonSchema,
|
||||
pub config: JsonDict,
|
||||
pub meta: JsonDict,
|
||||
pub tags: Vec<String>,
|
||||
pub runtime_deps: Vec<String>,
|
||||
pub is_standard: bool,
|
||||
}
|
||||
|
||||
/// Input for updating a pack
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdatePackInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub conf_schema: Option<JsonSchema>,
|
||||
pub config: Option<JsonDict>,
|
||||
pub meta: Option<JsonDict>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub runtime_deps: Option<Vec<String>>,
|
||||
pub is_standard: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for PackRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let pack = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(pack)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for PackRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let pack = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(pack)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for PackRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let packs = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(packs)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for PackRepository {
|
||||
type CreateInput = CreatePackInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Validate ref format (alphanumeric, dots, underscores, hyphens)
|
||||
if !input
|
||||
.r#ref
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')
|
||||
{
|
||||
return Err(Error::validation(
|
||||
"Pack ref must contain only alphanumeric characters, dots, underscores, and hyphens",
|
||||
));
|
||||
}
|
||||
|
||||
// Try to insert - database will enforce uniqueness constraint
|
||||
let pack = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
INSERT INTO pack (ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(&input.version)
|
||||
.bind(&input.conf_schema)
|
||||
.bind(&input.config)
|
||||
.bind(&input.meta)
|
||||
.bind(&input.tags)
|
||||
.bind(&input.runtime_deps)
|
||||
.bind(input.is_standard)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert unique constraint violation to AlreadyExists error
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.is_unique_violation() {
|
||||
return Error::already_exists("Pack", "ref", &input.r#ref);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(pack)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for PackRepository {
|
||||
type UpdateInput = UpdatePackInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
// Build dynamic UPDATE query
|
||||
let mut query = QueryBuilder::new("UPDATE pack SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("label = ");
|
||||
query.push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(version) = &input.version {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("version = ");
|
||||
query.push_bind(version);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(conf_schema) = &input.conf_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("conf_schema = ");
|
||||
query.push_bind(conf_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(config) = &input.config {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("config = ");
|
||||
query.push_bind(config);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(meta) = &input.meta {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("meta = ");
|
||||
query.push_bind(meta);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(tags) = &input.tags {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("tags = ");
|
||||
query.push_bind(tags);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(runtime_deps) = &input.runtime_deps {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("runtime_deps = ");
|
||||
query.push_bind(runtime_deps);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(is_standard) = input.is_standard {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("is_standard = ");
|
||||
query.push_bind(is_standard);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
// No updates requested, fetch and return existing pack
|
||||
return Self::find_by_id(executor, id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::not_found("pack", "id", id.to_string()));
|
||||
}
|
||||
|
||||
// Add updated timestamp
|
||||
query.push(", updated = NOW() WHERE id = ");
|
||||
query.push_bind(id);
|
||||
query.push(" RETURNING id, ref, label, description, version, conf_schema, config, meta, tags, runtime_deps, is_standard, created, updated");
|
||||
|
||||
let pack = query
|
||||
.build_query_as::<Pack>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
sqlx::Error::RowNotFound => Error::not_found("pack", "id", id.to_string()),
|
||||
_ => e.into(),
|
||||
})?;
|
||||
|
||||
Ok(pack)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for PackRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM pack WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PackRepository {
|
||||
/// List packs with pagination
|
||||
pub async fn list_paginated<'e, E>(executor: E, pagination: Pagination) -> Result<Vec<Pack>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let packs = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
ORDER BY ref ASC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
)
|
||||
.bind(pagination.limit())
|
||||
.bind(pagination.offset())
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
/// Count total number of packs
|
||||
pub async fn count<'e, E>(executor: E) -> Result<i64>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM pack")
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(count.0)
|
||||
}
|
||||
|
||||
/// Find packs by tag
|
||||
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<Pack>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let packs = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
WHERE $1 = ANY(tags)
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(tag)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
/// Find standard packs
|
||||
pub async fn find_standard<'e, E>(executor: E) -> Result<Vec<Pack>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let packs = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
WHERE is_standard = true
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
/// Search packs by name/label (case-insensitive)
|
||||
pub async fn search<'e, E>(executor: E, query: &str) -> Result<Vec<Pack>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let search_pattern = format!("%{}%", query.to_lowercase());
|
||||
let packs = sqlx::query_as::<_, Pack>(
|
||||
r#"
|
||||
SELECT id, ref, label, description, version, conf_schema, config, meta,
|
||||
tags, runtime_deps, is_standard, created, updated
|
||||
FROM pack
|
||||
WHERE LOWER(ref) LIKE $1 OR LOWER(label) LIKE $1 OR LOWER(description) LIKE $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(&search_pattern)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(packs)
|
||||
}
|
||||
|
||||
/// Check if a pack with the given ref exists
|
||||
pub async fn exists_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let exists: (bool,) =
|
||||
sqlx::query_as("SELECT EXISTS(SELECT 1 FROM pack WHERE ref = $1)")
|
||||
.bind(ref_str)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(exists.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_pack_input() {
|
||||
let input = CreatePackInput {
|
||||
r#ref: "test.pack".to_string(),
|
||||
label: "Test Pack".to_string(),
|
||||
description: Some("A test pack".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
conf_schema: serde_json::json!({}),
|
||||
config: serde_json::json!({}),
|
||||
meta: serde_json::json!({}),
|
||||
tags: vec!["test".to_string()],
|
||||
runtime_deps: vec![],
|
||||
is_standard: false,
|
||||
};
|
||||
|
||||
assert_eq!(input.r#ref, "test.pack");
|
||||
assert_eq!(input.label, "Test Pack");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_pack_input_default() {
|
||||
let input = UpdatePackInput::default();
|
||||
assert!(input.label.is_none());
|
||||
assert!(input.description.is_none());
|
||||
assert!(input.version.is_none());
|
||||
}
|
||||
}
|
||||
173
crates/common/src/repositories/pack_installation.rs
Normal file
173
crates/common/src/repositories/pack_installation.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Pack Installation Repository
|
||||
//!
|
||||
//! This module provides database operations for pack installation metadata.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models::{CreatePackInstallation, Id, PackInstallation};
|
||||
use sqlx::PgPool;
|
||||
|
||||
/// Repository for pack installation metadata operations
|
||||
pub struct PackInstallationRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PackInstallationRepository {
|
||||
/// Create a new PackInstallationRepository
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
/// Create a new pack installation record
|
||||
pub async fn create(&self, data: CreatePackInstallation) -> Result<PackInstallation> {
|
||||
let installation = sqlx::query_as::<_, PackInstallation>(
|
||||
r#"
|
||||
INSERT INTO pack_installation (
|
||||
pack_id, source_type, source_url, source_ref,
|
||||
checksum, checksum_verified, installed_by,
|
||||
installation_method, storage_path, meta
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(data.pack_id)
|
||||
.bind(&data.source_type)
|
||||
.bind(&data.source_url)
|
||||
.bind(&data.source_ref)
|
||||
.bind(&data.checksum)
|
||||
.bind(data.checksum_verified)
|
||||
.bind(data.installed_by)
|
||||
.bind(&data.installation_method)
|
||||
.bind(&data.storage_path)
|
||||
.bind(data.meta.unwrap_or_else(|| serde_json::json!({})))
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Get pack installation by ID
|
||||
pub async fn get_by_id(&self, id: Id) -> Result<Option<PackInstallation>> {
|
||||
let installation =
|
||||
sqlx::query_as::<_, PackInstallation>("SELECT * FROM pack_installation WHERE id = $1")
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Get pack installation by pack ID
|
||||
pub async fn get_by_pack_id(&self, pack_id: Id) -> Result<Option<PackInstallation>> {
|
||||
let installation = sqlx::query_as::<_, PackInstallation>(
|
||||
"SELECT * FROM pack_installation WHERE pack_id = $1",
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// List all pack installations
|
||||
pub async fn list(&self) -> Result<Vec<PackInstallation>> {
|
||||
let installations = sqlx::query_as::<_, PackInstallation>(
|
||||
"SELECT * FROM pack_installation ORDER BY installed_at DESC",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installations)
|
||||
}
|
||||
|
||||
/// List pack installations by source type
|
||||
pub async fn list_by_source_type(&self, source_type: &str) -> Result<Vec<PackInstallation>> {
|
||||
let installations = sqlx::query_as::<_, PackInstallation>(
|
||||
"SELECT * FROM pack_installation WHERE source_type = $1 ORDER BY installed_at DESC",
|
||||
)
|
||||
.bind(source_type)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installations)
|
||||
}
|
||||
|
||||
/// Update pack installation checksum
|
||||
pub async fn update_checksum(
|
||||
&self,
|
||||
id: Id,
|
||||
checksum: &str,
|
||||
verified: bool,
|
||||
) -> Result<PackInstallation> {
|
||||
let installation = sqlx::query_as::<_, PackInstallation>(
|
||||
r#"
|
||||
UPDATE pack_installation
|
||||
SET checksum = $2, checksum_verified = $3
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(checksum)
|
||||
.bind(verified)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Update pack installation metadata
|
||||
pub async fn update_meta(&self, id: Id, meta: serde_json::Value) -> Result<PackInstallation> {
|
||||
let installation = sqlx::query_as::<_, PackInstallation>(
|
||||
r#"
|
||||
UPDATE pack_installation
|
||||
SET meta = $2
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(meta)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(installation)
|
||||
}
|
||||
|
||||
/// Delete pack installation by ID
|
||||
pub async fn delete(&self, id: Id) -> Result<()> {
|
||||
sqlx::query("DELETE FROM pack_installation WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete pack installation by pack ID
|
||||
pub async fn delete_by_pack_id(&self, pack_id: Id) -> Result<()> {
|
||||
sqlx::query("DELETE FROM pack_installation WHERE pack_id = $1")
|
||||
.bind(pack_id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a pack has installation metadata
|
||||
pub async fn exists_for_pack(&self, pack_id: Id) -> Result<bool> {
|
||||
let count: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM pack_installation WHERE pack_id = $1")
|
||||
.bind(pack_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(count.0 > 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Note: Integration tests should be added in tests/ directory
|
||||
// These would require a test database setup
|
||||
}
|
||||
409
crates/common/src/repositories/pack_test.rs
Normal file
409
crates/common/src/repositories/pack_test.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
//! Pack Test Repository
|
||||
//!
|
||||
//! Database operations for pack test execution tracking.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models::{Id, PackLatestTest, PackTestExecution, PackTestResult, PackTestStats};
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
/// Repository for pack test operations
|
||||
pub struct PackTestRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PackTestRepository {
|
||||
/// Create a new pack test repository
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
/// Create a new pack test execution record
|
||||
pub async fn create(
|
||||
&self,
|
||||
pack_id: Id,
|
||||
pack_version: &str,
|
||||
trigger_reason: &str,
|
||||
result: &PackTestResult,
|
||||
) -> Result<PackTestExecution> {
|
||||
let result_json = serde_json::to_value(result)?;
|
||||
|
||||
let record = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
INSERT INTO pack_test_execution (
|
||||
pack_id, pack_version, execution_time, trigger_reason,
|
||||
total_tests, passed, failed, skipped, pass_rate, duration_ms, result
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.bind(pack_version)
|
||||
.bind(result.execution_time)
|
||||
.bind(trigger_reason)
|
||||
.bind(result.total_tests)
|
||||
.bind(result.passed)
|
||||
.bind(result.failed)
|
||||
.bind(result.skipped)
|
||||
.bind(result.pass_rate)
|
||||
.bind(result.duration_ms)
|
||||
.bind(result_json)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Find pack test execution by ID
|
||||
pub async fn find_by_id(&self, id: Id) -> Result<Option<PackTestExecution>> {
|
||||
let record = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
SELECT * FROM pack_test_execution
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// List all test executions for a pack
|
||||
pub async fn list_by_pack(
|
||||
&self,
|
||||
pack_id: Id,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<PackTestExecution>> {
|
||||
let records = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
SELECT * FROM pack_test_execution
|
||||
WHERE pack_id = $1
|
||||
ORDER BY execution_time DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Get latest test execution for a pack
|
||||
pub async fn get_latest_by_pack(&self, pack_id: Id) -> Result<Option<PackTestExecution>> {
|
||||
let record = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
SELECT * FROM pack_test_execution
|
||||
WHERE pack_id = $1
|
||||
ORDER BY execution_time DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
/// Get latest test for all packs
|
||||
pub async fn get_all_latest(&self) -> Result<Vec<PackLatestTest>> {
|
||||
let records = sqlx::query_as::<_, PackLatestTest>(
|
||||
r#"
|
||||
SELECT * FROM pack_latest_test
|
||||
ORDER BY test_time DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Get test statistics for a pack
|
||||
pub async fn get_stats(&self, pack_id: Id) -> Result<PackTestStats> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT * FROM get_pack_test_stats($1)
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(PackTestStats {
|
||||
total_executions: row.get("total_executions"),
|
||||
successful_executions: row.get("successful_executions"),
|
||||
failed_executions: row.get("failed_executions"),
|
||||
avg_pass_rate: row.get("avg_pass_rate"),
|
||||
avg_duration_ms: row.get("avg_duration_ms"),
|
||||
last_test_time: row.get("last_test_time"),
|
||||
last_test_passed: row.get("last_test_passed"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if pack has recent passing tests
|
||||
pub async fn has_passing_tests(&self, pack_id: Id, hours_ago: i32) -> Result<bool> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT pack_has_passing_tests($1, $2) as has_passing
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.bind(hours_ago)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.get("has_passing"))
|
||||
}
|
||||
|
||||
/// Count test executions by pack
|
||||
pub async fn count_by_pack(&self, pack_id: Id) -> Result<i64> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) as count FROM pack_test_execution
|
||||
WHERE pack_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.get("count"))
|
||||
}
|
||||
|
||||
/// List test executions by trigger reason
|
||||
pub async fn list_by_trigger_reason(
|
||||
&self,
|
||||
trigger_reason: &str,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<PackTestExecution>> {
|
||||
let records = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
SELECT * FROM pack_test_execution
|
||||
WHERE trigger_reason = $1
|
||||
ORDER BY execution_time DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_reason)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Get failed test executions for a pack
|
||||
pub async fn get_failed_by_pack(
|
||||
&self,
|
||||
pack_id: Id,
|
||||
limit: i64,
|
||||
) -> Result<Vec<PackTestExecution>> {
|
||||
let records = sqlx::query_as::<_, PackTestExecution>(
|
||||
r#"
|
||||
SELECT * FROM pack_test_execution
|
||||
WHERE pack_id = $1 AND failed > 0
|
||||
ORDER BY execution_time DESC
|
||||
LIMIT $2
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.bind(limit)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
|
||||
/// Delete old test executions (cleanup)
|
||||
pub async fn delete_old_executions(&self, days_old: i32) -> Result<u64> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM pack_test_execution
|
||||
WHERE execution_time < NOW() - ($1 || ' days')::INTERVAL
|
||||
"#,
|
||||
)
|
||||
.bind(days_old)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Update these tests to use the new repository API (static methods)
|
||||
// These tests are currently disabled due to repository refactoring
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
mod tests {
|
||||
// Disabled - needs update for new repository API
|
||||
/*
|
||||
async fn setup() -> (PgPool, PackRepository, PackTestRepository) {
|
||||
let config = DatabaseConfig::from_env();
|
||||
let db = Database::new(&config)
|
||||
.await
|
||||
.expect("Failed to create database");
|
||||
let pool = db.pool().clone();
|
||||
let pack_repo = PackRepository::new(pool.clone());
|
||||
let test_repo = PackTestRepository::new(pool.clone());
|
||||
(pool, pack_repo, test_repo)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires database
|
||||
async fn test_create_test_execution() {
|
||||
let (_pool, pack_repo, test_repo) = setup().await;
|
||||
|
||||
// Create a test pack
|
||||
let pack = pack_repo
|
||||
.create("test_pack", "Test Pack", "Test pack for testing", "1.0.0")
|
||||
.await
|
||||
.expect("Failed to create pack");
|
||||
|
||||
// Create test result
|
||||
let test_result = PackTestResult {
|
||||
pack_ref: "test_pack".to_string(),
|
||||
pack_version: "1.0.0".to_string(),
|
||||
execution_time: Utc::now(),
|
||||
status: TestStatus::Passed,
|
||||
total_tests: 10,
|
||||
passed: 8,
|
||||
failed: 2,
|
||||
skipped: 0,
|
||||
pass_rate: 0.8,
|
||||
duration_ms: 5000,
|
||||
test_suites: vec![TestSuiteResult {
|
||||
name: "Test Suite 1".to_string(),
|
||||
runner_type: "shell".to_string(),
|
||||
total: 10,
|
||||
passed: 8,
|
||||
failed: 2,
|
||||
skipped: 0,
|
||||
duration_ms: 5000,
|
||||
test_cases: vec![
|
||||
TestCaseResult {
|
||||
name: "test_1".to_string(),
|
||||
status: TestStatus::Passed,
|
||||
duration_ms: 500,
|
||||
error_message: None,
|
||||
stdout: Some("Success".to_string()),
|
||||
stderr: None,
|
||||
},
|
||||
TestCaseResult {
|
||||
name: "test_2".to_string(),
|
||||
status: TestStatus::Failed,
|
||||
duration_ms: 300,
|
||||
error_message: Some("Test failed".to_string()),
|
||||
stdout: None,
|
||||
stderr: Some("Error output".to_string()),
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
// Create test execution
|
||||
let execution = test_repo
|
||||
.create(pack.id, "1.0.0", "manual", &test_result)
|
||||
.await
|
||||
.expect("Failed to create test execution");
|
||||
|
||||
assert_eq!(execution.pack_id, pack.id);
|
||||
assert_eq!(execution.total_tests, 10);
|
||||
assert_eq!(execution.passed, 8);
|
||||
assert_eq!(execution.failed, 2);
|
||||
assert_eq!(execution.pass_rate, 0.8);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires database
|
||||
async fn test_get_latest_by_pack() {
|
||||
let (_pool, pack_repo, test_repo) = setup().await;
|
||||
|
||||
// Create a test pack
|
||||
let pack = pack_repo
|
||||
.create("test_pack_2", "Test Pack 2", "Test pack 2", "1.0.0")
|
||||
.await
|
||||
.expect("Failed to create pack");
|
||||
|
||||
// Create multiple test executions
|
||||
for i in 1..=3 {
|
||||
let test_result = PackTestResult {
|
||||
pack_ref: "test_pack_2".to_string(),
|
||||
pack_version: "1.0.0".to_string(),
|
||||
execution_time: Utc::now(),
|
||||
total_tests: i,
|
||||
passed: i,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
pass_rate: 1.0,
|
||||
duration_ms: 1000,
|
||||
test_suites: vec![],
|
||||
};
|
||||
|
||||
test_repo
|
||||
.create(pack.id, "1.0.0", "manual", &test_result)
|
||||
.await
|
||||
.expect("Failed to create test execution");
|
||||
}
|
||||
|
||||
// Get latest
|
||||
let latest = test_repo
|
||||
.get_latest_by_pack(pack.id)
|
||||
.await
|
||||
.expect("Failed to get latest")
|
||||
.expect("No latest found");
|
||||
|
||||
assert_eq!(latest.total_tests, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires database
|
||||
async fn test_get_stats() {
|
||||
let (_pool, pack_repo, test_repo) = setup().await;
|
||||
|
||||
// Create a test pack
|
||||
let pack = pack_repo
|
||||
.create("test_pack_3", "Test Pack 3", "Test pack 3", "1.0.0")
|
||||
.await
|
||||
.expect("Failed to create pack");
|
||||
|
||||
// Create test executions
|
||||
for _ in 1..=5 {
|
||||
let test_result = PackTestResult {
|
||||
pack_ref: "test_pack_3".to_string(),
|
||||
pack_version: "1.0.0".to_string(),
|
||||
execution_time: Utc::now(),
|
||||
total_tests: 10,
|
||||
passed: 10,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
pass_rate: 1.0,
|
||||
duration_ms: 2000,
|
||||
test_suites: vec![],
|
||||
};
|
||||
|
||||
test_repo
|
||||
.create(pack.id, "1.0.0", "manual", &test_result)
|
||||
.await
|
||||
.expect("Failed to create test execution");
|
||||
}
|
||||
|
||||
// Get stats
|
||||
let stats = test_repo
|
||||
.get_stats(pack.id)
|
||||
.await
|
||||
.expect("Failed to get stats");
|
||||
|
||||
assert_eq!(stats.total_executions, 5);
|
||||
assert_eq!(stats.successful_executions, 5);
|
||||
assert_eq!(stats.failed_executions, 0);
|
||||
}
|
||||
*/
|
||||
}
|
||||
266
crates/common/src/repositories/queue_stats.rs
Normal file
266
crates/common/src/repositories/queue_stats.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
//! Queue Statistics Repository
|
||||
//!
|
||||
//! Provides database operations for queue statistics persistence.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::models::Id;
|
||||
|
||||
/// Queue statistics model
|
||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
||||
pub struct QueueStats {
|
||||
pub action_id: Id,
|
||||
pub queue_length: i32,
|
||||
pub active_count: i32,
|
||||
pub max_concurrent: i32,
|
||||
pub oldest_enqueued_at: Option<DateTime<Utc>>,
|
||||
pub total_enqueued: i64,
|
||||
pub total_completed: i64,
|
||||
pub last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Input for upserting queue statistics
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpsertQueueStatsInput {
|
||||
pub action_id: Id,
|
||||
pub queue_length: i32,
|
||||
pub active_count: i32,
|
||||
pub max_concurrent: i32,
|
||||
pub oldest_enqueued_at: Option<DateTime<Utc>>,
|
||||
pub total_enqueued: i64,
|
||||
pub total_completed: i64,
|
||||
}
|
||||
|
||||
/// Queue statistics repository
|
||||
pub struct QueueStatsRepository;
|
||||
|
||||
impl QueueStatsRepository {
|
||||
/// Upsert queue statistics (insert or update)
|
||||
pub async fn upsert(pool: &PgPool, input: UpsertQueueStatsInput) -> Result<QueueStats> {
|
||||
let stats = sqlx::query_as::<Postgres, QueueStats>(
|
||||
r#"
|
||||
INSERT INTO queue_stats (
|
||||
action_id,
|
||||
queue_length,
|
||||
active_count,
|
||||
max_concurrent,
|
||||
oldest_enqueued_at,
|
||||
total_enqueued,
|
||||
total_completed,
|
||||
last_updated
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||
ON CONFLICT (action_id) DO UPDATE SET
|
||||
queue_length = EXCLUDED.queue_length,
|
||||
active_count = EXCLUDED.active_count,
|
||||
max_concurrent = EXCLUDED.max_concurrent,
|
||||
oldest_enqueued_at = EXCLUDED.oldest_enqueued_at,
|
||||
total_enqueued = EXCLUDED.total_enqueued,
|
||||
total_completed = EXCLUDED.total_completed,
|
||||
last_updated = NOW()
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(input.action_id)
|
||||
.bind(input.queue_length)
|
||||
.bind(input.active_count)
|
||||
.bind(input.max_concurrent)
|
||||
.bind(input.oldest_enqueued_at)
|
||||
.bind(input.total_enqueued)
|
||||
.bind(input.total_completed)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Get queue statistics for a specific action
|
||||
pub async fn find_by_action(pool: &PgPool, action_id: Id) -> Result<Option<QueueStats>> {
|
||||
let stats = sqlx::query_as::<Postgres, QueueStats>(
|
||||
r#"
|
||||
SELECT
|
||||
action_id,
|
||||
queue_length,
|
||||
active_count,
|
||||
max_concurrent,
|
||||
oldest_enqueued_at,
|
||||
total_enqueued,
|
||||
total_completed,
|
||||
last_updated
|
||||
FROM queue_stats
|
||||
WHERE action_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// List all queue statistics with active queues (queue_length > 0 or active_count > 0)
|
||||
pub async fn list_active(pool: &PgPool) -> Result<Vec<QueueStats>> {
|
||||
let stats = sqlx::query_as::<Postgres, QueueStats>(
|
||||
r#"
|
||||
SELECT
|
||||
action_id,
|
||||
queue_length,
|
||||
active_count,
|
||||
max_concurrent,
|
||||
oldest_enqueued_at,
|
||||
total_enqueued,
|
||||
total_completed,
|
||||
last_updated
|
||||
FROM queue_stats
|
||||
WHERE queue_length > 0 OR active_count > 0
|
||||
ORDER BY last_updated DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// List all queue statistics
|
||||
pub async fn list_all(pool: &PgPool) -> Result<Vec<QueueStats>> {
|
||||
let stats = sqlx::query_as::<Postgres, QueueStats>(
|
||||
r#"
|
||||
SELECT
|
||||
action_id,
|
||||
queue_length,
|
||||
active_count,
|
||||
max_concurrent,
|
||||
oldest_enqueued_at,
|
||||
total_enqueued,
|
||||
total_completed,
|
||||
last_updated
|
||||
FROM queue_stats
|
||||
ORDER BY last_updated DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Delete queue statistics for a specific action
|
||||
pub async fn delete(pool: &PgPool, action_id: Id) -> Result<bool> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM queue_stats
|
||||
WHERE action_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
/// Batch upsert multiple queue statistics
|
||||
pub async fn batch_upsert(
|
||||
pool: &PgPool,
|
||||
inputs: Vec<UpsertQueueStatsInput>,
|
||||
) -> Result<Vec<QueueStats>> {
|
||||
if inputs.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Build dynamic query for batch insert
|
||||
let mut query_builder = QueryBuilder::new(
|
||||
r#"
|
||||
INSERT INTO queue_stats (
|
||||
action_id,
|
||||
queue_length,
|
||||
active_count,
|
||||
max_concurrent,
|
||||
oldest_enqueued_at,
|
||||
total_enqueued,
|
||||
total_completed,
|
||||
last_updated
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
query_builder.push_values(inputs.iter(), |mut b, input| {
|
||||
b.push_bind(input.action_id)
|
||||
.push_bind(input.queue_length)
|
||||
.push_bind(input.active_count)
|
||||
.push_bind(input.max_concurrent)
|
||||
.push_bind(input.oldest_enqueued_at)
|
||||
.push_bind(input.total_enqueued)
|
||||
.push_bind(input.total_completed)
|
||||
.push("NOW()");
|
||||
});
|
||||
|
||||
query_builder.push(
|
||||
r#"
|
||||
ON CONFLICT (action_id) DO UPDATE SET
|
||||
queue_length = EXCLUDED.queue_length,
|
||||
active_count = EXCLUDED.active_count,
|
||||
max_concurrent = EXCLUDED.max_concurrent,
|
||||
oldest_enqueued_at = EXCLUDED.oldest_enqueued_at,
|
||||
total_enqueued = EXCLUDED.total_enqueued,
|
||||
total_completed = EXCLUDED.total_completed,
|
||||
last_updated = NOW()
|
||||
RETURNING *
|
||||
"#,
|
||||
);
|
||||
|
||||
let stats = query_builder
|
||||
.build_query_as::<QueueStats>()
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Clear stale statistics (older than specified duration)
|
||||
pub async fn clear_stale(pool: &PgPool, older_than_seconds: i64) -> Result<u64> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM queue_stats
|
||||
WHERE last_updated < NOW() - INTERVAL '1 second' * $1
|
||||
AND queue_length = 0
|
||||
AND active_count = 0
|
||||
"#,
|
||||
)
|
||||
.bind(older_than_seconds)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_queue_stats_structure() {
|
||||
let input = UpsertQueueStatsInput {
|
||||
action_id: 1,
|
||||
queue_length: 5,
|
||||
active_count: 2,
|
||||
max_concurrent: 3,
|
||||
oldest_enqueued_at: Some(Utc::now()),
|
||||
total_enqueued: 100,
|
||||
total_completed: 95,
|
||||
};
|
||||
|
||||
assert_eq!(input.action_id, 1);
|
||||
assert_eq!(input.queue_length, 5);
|
||||
assert_eq!(input.active_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_batch_upsert() {
|
||||
let inputs: Vec<UpsertQueueStatsInput> = Vec::new();
|
||||
assert_eq!(inputs.len(), 0);
|
||||
}
|
||||
}
|
||||
340
crates/common/src/repositories/rule.rs
Normal file
340
crates/common/src/repositories/rule.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
//! Rule repository for database operations
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Rule entities.
|
||||
|
||||
use crate::models::{rule::*, Id};
|
||||
use crate::{Error, Result};
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
/// Repository for Rule operations
|
||||
pub struct RuleRepository;
|
||||
|
||||
impl Repository for RuleRepository {
|
||||
type Entity = Rule;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"rules"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new rule
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateRuleInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub action: Id,
|
||||
pub action_ref: String,
|
||||
pub trigger: Id,
|
||||
pub trigger_ref: String,
|
||||
pub conditions: serde_json::Value,
|
||||
pub action_params: serde_json::Value,
|
||||
pub trigger_params: serde_json::Value,
|
||||
pub enabled: bool,
|
||||
pub is_adhoc: bool,
|
||||
}
|
||||
|
||||
/// Input for updating a rule
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateRuleInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub conditions: Option<serde_json::Value>,
|
||||
pub action_params: Option<serde_json::Value>,
|
||||
pub trigger_params: Option<serde_json::Value>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for RuleRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rule = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for RuleRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rule = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for RuleRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rules = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for RuleRepository {
|
||||
type CreateInput = CreateRuleInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rule = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
INSERT INTO rule (ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(input.action)
|
||||
.bind(&input.action_ref)
|
||||
.bind(input.trigger)
|
||||
.bind(&input.trigger_ref)
|
||||
.bind(&input.conditions)
|
||||
.bind(&input.action_params)
|
||||
.bind(&input.trigger_params)
|
||||
.bind(input.enabled)
|
||||
.bind(input.is_adhoc)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let sqlx::Error::Database(ref db_err) = e {
|
||||
if db_err.is_unique_violation() {
|
||||
return Error::already_exists("Rule", "ref", &input.r#ref);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for RuleRepository {
|
||||
type UpdateInput = UpdateRuleInput;
|
||||
|
||||
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 rule SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
query.push("label = ");
|
||||
query.push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(conditions) = &input.conditions {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("conditions = ");
|
||||
query.push_bind(conditions);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(action_params) = &input.action_params {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("action_params = ");
|
||||
query.push_bind(action_params);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(trigger_params) = &input.trigger_params {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("trigger_params = ");
|
||||
query.push_bind(trigger_params);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(enabled) = input.enabled {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("enabled = ");
|
||||
query.push_bind(enabled);
|
||||
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, ref, pack, pack_ref, label, description, action, action_ref, trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated");
|
||||
|
||||
let rule = query.build_query_as::<Rule>().fetch_one(executor).await?;
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for RuleRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM rule WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl RuleRepository {
|
||||
/// Find rules by pack ID
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Rule>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rules = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Find rules by action ID
|
||||
pub async fn find_by_action<'e, E>(executor: E, action_id: Id) -> Result<Vec<Rule>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rules = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE action = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(action_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Find rules by trigger ID
|
||||
pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result<Vec<Rule>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rules = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE trigger = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
|
||||
/// Find enabled rules
|
||||
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<Rule>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let rules = sqlx::query_as::<_, Rule>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, action, action_ref,
|
||||
trigger, trigger_ref, conditions, action_params, trigger_params, enabled, is_adhoc, created, updated
|
||||
FROM rule
|
||||
WHERE enabled = true
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(rules)
|
||||
}
|
||||
}
|
||||
549
crates/common/src/repositories/runtime.rs
Normal file
549
crates/common/src/repositories/runtime.rs
Normal file
@@ -0,0 +1,549 @@
|
||||
//! Runtime and Worker repository for database operations
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Runtime and Worker entities.
|
||||
|
||||
use crate::models::{
|
||||
enums::{WorkerStatus, WorkerType},
|
||||
runtime::*,
|
||||
Id, JsonDict,
|
||||
};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
/// Repository for Runtime operations
|
||||
pub struct RuntimeRepository;
|
||||
|
||||
impl Repository for RuntimeRepository {
|
||||
type Entity = Runtime;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"runtime"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new runtime
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateRuntimeInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub name: String,
|
||||
pub distributions: JsonDict,
|
||||
pub installation: Option<JsonDict>,
|
||||
}
|
||||
|
||||
/// Input for updating a runtime
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateRuntimeInput {
|
||||
pub description: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub distributions: Option<JsonDict>,
|
||||
pub installation: Option<JsonDict>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for RuntimeRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let runtime = sqlx::query_as::<_, Runtime>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers, created, updated
|
||||
FROM runtime
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for RuntimeRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let runtime = sqlx::query_as::<_, Runtime>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers, created, updated
|
||||
FROM runtime
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for RuntimeRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let runtimes = sqlx::query_as::<_, Runtime>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers, created, updated
|
||||
FROM runtime
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtimes)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for RuntimeRepository {
|
||||
type CreateInput = CreateRuntimeInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let runtime = sqlx::query_as::<_, Runtime>(
|
||||
r#"
|
||||
INSERT INTO runtime (ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.description)
|
||||
.bind(&input.name)
|
||||
.bind(&input.distributions)
|
||||
.bind(&input.installation)
|
||||
.bind(serde_json::json!({}))
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for RuntimeRepository {
|
||||
type UpdateInput = UpdateRuntimeInput;
|
||||
|
||||
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 runtime SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(name) = &input.name {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("name = ");
|
||||
query.push_bind(name);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(distributions) = &input.distributions {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("distributions = ");
|
||||
query.push_bind(distributions);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(installation) = &input.installation {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("installation = ");
|
||||
query.push_bind(installation);
|
||||
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, ref, pack, pack_ref, description, name, distributions, installation, installers, created, updated");
|
||||
|
||||
let runtime = query
|
||||
.build_query_as::<Runtime>()
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for RuntimeRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM runtime WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeRepository {
|
||||
/// Find runtimes by pack
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Runtime>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let runtimes = sqlx::query_as::<_, Runtime>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, description, name,
|
||||
distributions, installation, installers, created, updated
|
||||
FROM runtime
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(runtimes)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Worker Repository
|
||||
// ============================================================================
|
||||
|
||||
/// Repository for Worker operations
|
||||
pub struct WorkerRepository;
|
||||
|
||||
impl Repository for WorkerRepository {
|
||||
type Entity = Worker;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"worker"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new worker
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateWorkerInput {
|
||||
pub name: String,
|
||||
pub worker_type: WorkerType,
|
||||
pub runtime: Option<Id>,
|
||||
pub host: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub status: Option<WorkerStatus>,
|
||||
pub capabilities: Option<JsonDict>,
|
||||
pub meta: Option<JsonDict>,
|
||||
}
|
||||
|
||||
/// Input for updating a worker
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateWorkerInput {
|
||||
pub name: Option<String>,
|
||||
pub status: Option<WorkerStatus>,
|
||||
pub capabilities: Option<JsonDict>,
|
||||
pub meta: Option<JsonDict>,
|
||||
pub host: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for WorkerRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let worker = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(worker)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for WorkerRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let workers = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
ORDER BY name ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(workers)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for WorkerRepository {
|
||||
type CreateInput = CreateWorkerInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let worker = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
INSERT INTO worker (name, worker_type, runtime, host, port, status,
|
||||
capabilities, meta)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, name, worker_type, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.name)
|
||||
.bind(input.worker_type)
|
||||
.bind(input.runtime)
|
||||
.bind(&input.host)
|
||||
.bind(input.port)
|
||||
.bind(input.status)
|
||||
.bind(&input.capabilities)
|
||||
.bind(&input.meta)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(worker)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for WorkerRepository {
|
||||
type UpdateInput = UpdateWorkerInput;
|
||||
|
||||
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 worker SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(name) = &input.name {
|
||||
query.push("name = ");
|
||||
query.push_bind(name);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(status) = input.status {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("status = ");
|
||||
query.push_bind(status);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(capabilities) = &input.capabilities {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("capabilities = ");
|
||||
query.push_bind(capabilities);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(meta) = &input.meta {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("meta = ");
|
||||
query.push_bind(meta);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(host) = &input.host {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("host = ");
|
||||
query.push_bind(host);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(port) = input.port {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("port = ");
|
||||
query.push_bind(port);
|
||||
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, name, worker_type, runtime, host, port, status, capabilities, meta, last_heartbeat, created, updated");
|
||||
|
||||
let worker = query.build_query_as::<Worker>().fetch_one(executor).await?;
|
||||
|
||||
Ok(worker)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for WorkerRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM worker WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkerRepository {
|
||||
/// Find workers by status
|
||||
pub async fn find_by_status<'e, E>(executor: E, status: WorkerStatus) -> Result<Vec<Worker>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let workers = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
WHERE status = $1
|
||||
ORDER BY name ASC
|
||||
"#,
|
||||
)
|
||||
.bind(status)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(workers)
|
||||
}
|
||||
|
||||
/// Find workers by type
|
||||
pub async fn find_by_type<'e, E>(executor: E, worker_type: WorkerType) -> Result<Vec<Worker>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let workers = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
WHERE worker_type = $1
|
||||
ORDER BY name ASC
|
||||
"#,
|
||||
)
|
||||
.bind(worker_type)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(workers)
|
||||
}
|
||||
|
||||
/// Update worker heartbeat
|
||||
pub async fn update_heartbeat<'e, E>(executor: E, id: i64) -> Result<()>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query("UPDATE worker SET last_heartbeat = NOW() WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find workers by name
|
||||
pub async fn find_by_name<'e, E>(executor: E, name: &str) -> Result<Option<Worker>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let worker = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
WHERE name = $1
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(worker)
|
||||
}
|
||||
|
||||
/// Find workers that can execute actions (role = 'action')
|
||||
pub async fn find_action_workers<'e, E>(executor: E) -> Result<Vec<Worker>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let workers = sqlx::query_as::<_, Worker>(
|
||||
r#"
|
||||
SELECT id, name, worker_type, worker_role, runtime, host, port, status,
|
||||
capabilities, meta, last_heartbeat, created, updated
|
||||
FROM worker
|
||||
WHERE worker_role = 'action'
|
||||
ORDER BY name ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(workers)
|
||||
}
|
||||
}
|
||||
795
crates/common/src/repositories/trigger.rs
Normal file
795
crates/common/src/repositories/trigger.rs
Normal file
@@ -0,0 +1,795 @@
|
||||
//! Trigger and Sensor repository for database operations
|
||||
//!
|
||||
//! This module provides CRUD operations and queries for Trigger and Sensor entities.
|
||||
|
||||
use crate::models::{trigger::*, Id, JsonSchema};
|
||||
use crate::Result;
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
/// Repository for Trigger operations
|
||||
pub struct TriggerRepository;
|
||||
|
||||
impl Repository for TriggerRepository {
|
||||
type Entity = Trigger;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"triggers"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new trigger
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateTriggerInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub label: String,
|
||||
pub description: Option<String>,
|
||||
pub enabled: bool,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
pub is_adhoc: bool,
|
||||
}
|
||||
|
||||
/// Input for updating a trigger
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateTriggerInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub enabled: Option<bool>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for TriggerRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let trigger = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for TriggerRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let trigger = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for TriggerRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let triggers = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(triggers)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for TriggerRepository {
|
||||
type CreateInput = CreateTriggerInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let trigger = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
INSERT INTO trigger (ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, is_adhoc)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(input.enabled)
|
||||
.bind(&input.param_schema)
|
||||
.bind(&input.out_schema)
|
||||
.bind(input.is_adhoc)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert unique constraint violation to AlreadyExists error
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.is_unique_violation() {
|
||||
return crate::Error::already_exists("Trigger", "ref", &input.r#ref);
|
||||
}
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for TriggerRepository {
|
||||
type UpdateInput = UpdateTriggerInput;
|
||||
|
||||
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 trigger SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
query.push("label = ");
|
||||
query.push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(enabled) = input.enabled {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("enabled = ");
|
||||
query.push_bind(enabled);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(param_schema) = &input.param_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("param_schema = ");
|
||||
query.push_bind(param_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(out_schema) = &input.out_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("out_schema = ");
|
||||
query.push_bind(out_schema);
|
||||
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, ref, pack, pack_ref, label, description, enabled, param_schema, out_schema, webhook_enabled, webhook_key, webhook_config, is_adhoc, created, updated");
|
||||
|
||||
let trigger = query
|
||||
.build_query_as::<Trigger>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert RowNotFound to NotFound error
|
||||
if matches!(e, sqlx::Error::RowNotFound) {
|
||||
return crate::Error::not_found("trigger", "id", &id.to_string());
|
||||
}
|
||||
e.into()
|
||||
})?;
|
||||
|
||||
Ok(trigger)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for TriggerRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM trigger WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TriggerRepository {
|
||||
/// Find triggers by pack ID
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Trigger>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let triggers = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(triggers)
|
||||
}
|
||||
|
||||
/// Find enabled triggers
|
||||
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<Trigger>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let triggers = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
WHERE enabled = true
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(triggers)
|
||||
}
|
||||
|
||||
/// Find trigger by webhook key
|
||||
pub async fn find_by_webhook_key<'e, E>(
|
||||
executor: E,
|
||||
webhook_key: &str,
|
||||
) -> Result<Option<Trigger>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let trigger = sqlx::query_as::<_, Trigger>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, enabled,
|
||||
param_schema, out_schema, webhook_enabled, webhook_key, webhook_config,
|
||||
is_adhoc, created, updated
|
||||
FROM trigger
|
||||
WHERE webhook_key = $1
|
||||
"#,
|
||||
)
|
||||
.bind(webhook_key)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(trigger)
|
||||
}
|
||||
|
||||
/// Enable webhooks for a trigger
|
||||
pub async fn enable_webhook<'e, E>(executor: E, trigger_id: Id) -> Result<WebhookInfo>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct WebhookResult {
|
||||
webhook_enabled: bool,
|
||||
webhook_key: String,
|
||||
webhook_url: String,
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, WebhookResult>(
|
||||
r#"
|
||||
SELECT * FROM enable_trigger_webhook($1)
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(WebhookInfo {
|
||||
enabled: result.webhook_enabled,
|
||||
webhook_key: result.webhook_key,
|
||||
webhook_url: result.webhook_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Disable webhooks for a trigger
|
||||
pub async fn disable_webhook<'e, E>(executor: E, trigger_id: Id) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query_scalar::<_, bool>(
|
||||
r#"
|
||||
SELECT disable_trigger_webhook($1)
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Regenerate webhook key for a trigger
|
||||
pub async fn regenerate_webhook_key<'e, E>(
|
||||
executor: E,
|
||||
trigger_id: Id,
|
||||
) -> Result<WebhookKeyRegenerate>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RegenerateResult {
|
||||
webhook_key: String,
|
||||
previous_key_revoked: bool,
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, RegenerateResult>(
|
||||
r#"
|
||||
SELECT * FROM regenerate_trigger_webhook_key($1)
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(WebhookKeyRegenerate {
|
||||
webhook_key: result.webhook_key,
|
||||
previous_key_revoked: result.previous_key_revoked,
|
||||
})
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Phase 3: Advanced Webhook Features
|
||||
// ========================================================================
|
||||
|
||||
/// Update webhook configuration for a trigger
|
||||
pub async fn update_webhook_config<'e, E>(
|
||||
executor: E,
|
||||
trigger_id: Id,
|
||||
config: serde_json::Value,
|
||||
) -> Result<()>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE trigger
|
||||
SET webhook_config = $2, updated = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.bind(config)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Log webhook event for auditing and analytics
|
||||
pub async fn log_webhook_event<'e, E>(executor: E, input: WebhookEventLogInput) -> Result<i64>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let id = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
INSERT INTO webhook_event_log (
|
||||
trigger_id, trigger_ref, webhook_key, event_id,
|
||||
source_ip, user_agent, payload_size_bytes, headers,
|
||||
status_code, error_message, processing_time_ms,
|
||||
hmac_verified, rate_limited, ip_allowed
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(input.trigger_id)
|
||||
.bind(input.trigger_ref)
|
||||
.bind(input.webhook_key)
|
||||
.bind(input.event_id)
|
||||
.bind(input.source_ip)
|
||||
.bind(input.user_agent)
|
||||
.bind(input.payload_size_bytes)
|
||||
.bind(input.headers)
|
||||
.bind(input.status_code)
|
||||
.bind(input.error_message)
|
||||
.bind(input.processing_time_ms)
|
||||
.bind(input.hmac_verified)
|
||||
.bind(input.rate_limited)
|
||||
.bind(input.ip_allowed)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Webhook information returned when enabling webhooks
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WebhookInfo {
|
||||
pub enabled: bool,
|
||||
pub webhook_key: String,
|
||||
pub webhook_url: String,
|
||||
}
|
||||
|
||||
/// Webhook key regeneration result
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WebhookKeyRegenerate {
|
||||
pub webhook_key: String,
|
||||
pub previous_key_revoked: bool,
|
||||
}
|
||||
|
||||
/// Input for logging webhook events
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebhookEventLogInput {
|
||||
pub trigger_id: Id,
|
||||
pub trigger_ref: String,
|
||||
pub webhook_key: String,
|
||||
pub event_id: Option<Id>,
|
||||
pub source_ip: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub payload_size_bytes: Option<i32>,
|
||||
pub headers: Option<JsonValue>,
|
||||
pub status_code: i32,
|
||||
pub error_message: Option<String>,
|
||||
pub processing_time_ms: Option<i32>,
|
||||
pub hmac_verified: Option<bool>,
|
||||
pub rate_limited: bool,
|
||||
pub ip_allowed: Option<bool>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sensor Repository
|
||||
// ============================================================================
|
||||
|
||||
/// Repository for Sensor operations
|
||||
pub struct SensorRepository;
|
||||
|
||||
impl Repository for SensorRepository {
|
||||
type Entity = Sensor;
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"sensor"
|
||||
}
|
||||
}
|
||||
|
||||
/// Input for creating a new sensor
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateSensorInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Option<Id>,
|
||||
pub pack_ref: Option<String>,
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
pub entrypoint: String,
|
||||
pub runtime: Id,
|
||||
pub runtime_ref: String,
|
||||
pub trigger: Id,
|
||||
pub trigger_ref: String,
|
||||
pub enabled: bool,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub config: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// Input for updating a sensor
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateSensorInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub entrypoint: Option<String>,
|
||||
pub enabled: Option<bool>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for SensorRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensor = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensor)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for SensorRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensor = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
WHERE ref = $1
|
||||
"#,
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensor)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for SensorRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensors = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensors)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for SensorRepository {
|
||||
type CreateInput = CreateSensorInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensor = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
INSERT INTO sensor (ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
"#,
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(&input.entrypoint)
|
||||
.bind(input.runtime)
|
||||
.bind(&input.runtime_ref)
|
||||
.bind(input.trigger)
|
||||
.bind(&input.trigger_ref)
|
||||
.bind(input.enabled)
|
||||
.bind(&input.param_schema)
|
||||
.bind(&input.config)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensor)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for SensorRepository {
|
||||
type UpdateInput = UpdateSensorInput;
|
||||
|
||||
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 sensor SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
query.push("label = ");
|
||||
query.push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ");
|
||||
query.push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(entrypoint) = &input.entrypoint {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("entrypoint = ");
|
||||
query.push_bind(entrypoint);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(enabled) = input.enabled {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("enabled = ");
|
||||
query.push_bind(enabled);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if let Some(param_schema) = &input.param_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("param_schema = ");
|
||||
query.push_bind(param_schema);
|
||||
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, ref, pack, pack_ref, label, description, entrypoint, runtime, runtime_ref, trigger, trigger_ref, enabled, param_schema, config, created, updated");
|
||||
|
||||
let sensor = query.build_query_as::<Sensor>().fetch_one(executor).await?;
|
||||
|
||||
Ok(sensor)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for SensorRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM sensor WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl SensorRepository {
|
||||
/// Find sensors by trigger ID
|
||||
pub async fn find_by_trigger<'e, E>(executor: E, trigger_id: Id) -> Result<Vec<Sensor>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensors = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
WHERE trigger = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(trigger_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensors)
|
||||
}
|
||||
|
||||
/// Find enabled sensors
|
||||
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<Sensor>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensors = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
WHERE enabled = true
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensors)
|
||||
}
|
||||
|
||||
/// Find sensors by pack ID
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<Sensor>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let sensors = sqlx::query_as::<_, Sensor>(
|
||||
r#"
|
||||
SELECT id, ref, pack, pack_ref, label, description, entrypoint,
|
||||
runtime, runtime_ref, trigger, trigger_ref, enabled,
|
||||
param_schema, config, created, updated
|
||||
FROM sensor
|
||||
WHERE pack = $1
|
||||
ORDER BY ref ASC
|
||||
"#,
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await?;
|
||||
|
||||
Ok(sensors)
|
||||
}
|
||||
}
|
||||
592
crates/common/src/repositories/workflow.rs
Normal file
592
crates/common/src/repositories/workflow.rs
Normal file
@@ -0,0 +1,592 @@
|
||||
//! Workflow repository for database operations
|
||||
|
||||
use crate::models::{enums::ExecutionStatus, workflow::*, Id, JsonDict, JsonSchema};
|
||||
use crate::Result;
|
||||
use sqlx::{Executor, Postgres, QueryBuilder};
|
||||
|
||||
use super::{Create, Delete, FindById, FindByRef, List, Repository, Update};
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOW DEFINITION REPOSITORY
|
||||
// ============================================================================
|
||||
|
||||
pub struct WorkflowDefinitionRepository;
|
||||
|
||||
impl Repository for WorkflowDefinitionRepository {
|
||||
type Entity = WorkflowDefinition;
|
||||
fn table_name() -> &'static str {
|
||||
"workflow_definition"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateWorkflowDefinitionInput {
|
||||
pub r#ref: String,
|
||||
pub pack: Id,
|
||||
pub pack_ref: String,
|
||||
pub label: String,
|
||||
pub description: Option<String>,
|
||||
pub version: String,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
pub definition: JsonDict,
|
||||
pub tags: Vec<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateWorkflowDefinitionInput {
|
||||
pub label: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub param_schema: Option<JsonSchema>,
|
||||
pub out_schema: Option<JsonSchema>,
|
||||
pub definition: Option<JsonDict>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for WorkflowDefinitionRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindByRef for WorkflowDefinitionRepository {
|
||||
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE ref = $1"
|
||||
)
|
||||
.bind(ref_str)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for WorkflowDefinitionRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000"
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for WorkflowDefinitionRepository {
|
||||
type CreateInput = CreateWorkflowDefinitionInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"INSERT INTO workflow_definition
|
||||
(ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated"
|
||||
)
|
||||
.bind(&input.r#ref)
|
||||
.bind(input.pack)
|
||||
.bind(&input.pack_ref)
|
||||
.bind(&input.label)
|
||||
.bind(&input.description)
|
||||
.bind(&input.version)
|
||||
.bind(&input.param_schema)
|
||||
.bind(&input.out_schema)
|
||||
.bind(&input.definition)
|
||||
.bind(&input.tags)
|
||||
.bind(input.enabled)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for WorkflowDefinitionRepository {
|
||||
type UpdateInput = UpdateWorkflowDefinitionInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let mut query = QueryBuilder::new("UPDATE workflow_definition SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(label) = &input.label {
|
||||
query.push("label = ").push_bind(label);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(description) = &input.description {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("description = ").push_bind(description);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(version) = &input.version {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("version = ").push_bind(version);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(param_schema) = &input.param_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("param_schema = ").push_bind(param_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(out_schema) = &input.out_schema {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("out_schema = ").push_bind(out_schema);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(definition) = &input.definition {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("definition = ").push_bind(definition);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(tags) = &input.tags {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("tags = ").push_bind(tags);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(enabled) = input.enabled {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("enabled = ").push_bind(enabled);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
return Self::get_by_id(executor, id).await;
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||
query.push(" RETURNING id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<WorkflowDefinition>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for WorkflowDefinitionRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM workflow_definition WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowDefinitionRepository {
|
||||
/// Find all workflows for a specific pack by pack ID
|
||||
pub async fn find_by_pack<'e, E>(executor: E, pack_id: Id) -> Result<Vec<WorkflowDefinition>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE pack = $1
|
||||
ORDER BY label"
|
||||
)
|
||||
.bind(pack_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find all workflows for a specific pack by pack reference
|
||||
pub async fn find_by_pack_ref<'e, E>(
|
||||
executor: E,
|
||||
pack_ref: &str,
|
||||
) -> Result<Vec<WorkflowDefinition>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE pack_ref = $1
|
||||
ORDER BY label"
|
||||
)
|
||||
.bind(pack_ref)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Count workflows for a specific pack by pack reference
|
||||
pub async fn count_by_pack<'e, E>(executor: E, pack_ref: &str) -> Result<i64>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result: (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*) FROM workflow_definition WHERE pack_ref = $1")
|
||||
.bind(pack_ref)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
Ok(result.0)
|
||||
}
|
||||
|
||||
/// Find all enabled workflows
|
||||
pub async fn find_enabled<'e, E>(executor: E) -> Result<Vec<WorkflowDefinition>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE enabled = true
|
||||
ORDER BY label"
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find workflows by tag
|
||||
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<WorkflowDefinition>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowDefinition>(
|
||||
"SELECT id, ref, pack, pack_ref, label, description, version, param_schema, out_schema, definition, tags, enabled, created, updated
|
||||
FROM workflow_definition
|
||||
WHERE $1 = ANY(tags)
|
||||
ORDER BY label"
|
||||
)
|
||||
.bind(tag)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOW EXECUTION REPOSITORY
|
||||
// ============================================================================
|
||||
|
||||
pub struct WorkflowExecutionRepository;
|
||||
|
||||
impl Repository for WorkflowExecutionRepository {
|
||||
type Entity = WorkflowExecution;
|
||||
fn table_name() -> &'static str {
|
||||
"workflow_execution"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateWorkflowExecutionInput {
|
||||
pub execution: Id,
|
||||
pub workflow_def: Id,
|
||||
pub task_graph: JsonDict,
|
||||
pub variables: JsonDict,
|
||||
pub status: ExecutionStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct UpdateWorkflowExecutionInput {
|
||||
pub current_tasks: Option<Vec<String>>,
|
||||
pub completed_tasks: Option<Vec<String>>,
|
||||
pub failed_tasks: Option<Vec<String>>,
|
||||
pub skipped_tasks: Option<Vec<String>>,
|
||||
pub variables: Option<JsonDict>,
|
||||
pub status: Option<ExecutionStatus>,
|
||||
pub error_message: Option<String>,
|
||||
pub paused: Option<bool>,
|
||||
pub pause_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FindById for WorkflowExecutionRepository {
|
||||
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl List for WorkflowExecutionRepository {
|
||||
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
ORDER BY created DESC
|
||||
LIMIT 1000"
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Create for WorkflowExecutionRepository {
|
||||
type CreateInput = CreateWorkflowExecutionInput;
|
||||
|
||||
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"INSERT INTO workflow_execution
|
||||
(execution, workflow_def, task_graph, variables, status)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated"
|
||||
)
|
||||
.bind(input.execution)
|
||||
.bind(input.workflow_def)
|
||||
.bind(&input.task_graph)
|
||||
.bind(&input.variables)
|
||||
.bind(input.status)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Update for WorkflowExecutionRepository {
|
||||
type UpdateInput = UpdateWorkflowExecutionInput;
|
||||
|
||||
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let mut query = QueryBuilder::new("UPDATE workflow_execution SET ");
|
||||
let mut has_updates = false;
|
||||
|
||||
if let Some(current_tasks) = &input.current_tasks {
|
||||
query.push("current_tasks = ").push_bind(current_tasks);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(completed_tasks) = &input.completed_tasks {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("completed_tasks = ").push_bind(completed_tasks);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(failed_tasks) = &input.failed_tasks {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("failed_tasks = ").push_bind(failed_tasks);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(skipped_tasks) = &input.skipped_tasks {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("skipped_tasks = ").push_bind(skipped_tasks);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(variables) = &input.variables {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("variables = ").push_bind(variables);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(status) = input.status {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("status = ").push_bind(status);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(error_message) = &input.error_message {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("error_message = ").push_bind(error_message);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(paused) = input.paused {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("paused = ").push_bind(paused);
|
||||
has_updates = true;
|
||||
}
|
||||
if let Some(pause_reason) = &input.pause_reason {
|
||||
if has_updates {
|
||||
query.push(", ");
|
||||
}
|
||||
query.push("pause_reason = ").push_bind(pause_reason);
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
if !has_updates {
|
||||
return Self::get_by_id(executor, id).await;
|
||||
}
|
||||
|
||||
query.push(", updated = NOW() WHERE id = ").push_bind(id);
|
||||
query.push(" RETURNING id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks, variables, task_graph, status, error_message, paused, pause_reason, created, updated");
|
||||
|
||||
query
|
||||
.build_query_as::<WorkflowExecution>()
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Delete for WorkflowExecutionRepository {
|
||||
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
let result = sqlx::query("DELETE FROM workflow_execution WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkflowExecutionRepository {
|
||||
/// Find workflow execution by the parent execution ID
|
||||
pub async fn find_by_execution<'e, E>(
|
||||
executor: E,
|
||||
execution_id: Id,
|
||||
) -> Result<Option<WorkflowExecution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
WHERE execution = $1"
|
||||
)
|
||||
.bind(execution_id)
|
||||
.fetch_optional(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find all workflow executions by status
|
||||
pub async fn find_by_status<'e, E>(
|
||||
executor: E,
|
||||
status: ExecutionStatus,
|
||||
) -> Result<Vec<WorkflowExecution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
WHERE status = $1
|
||||
ORDER BY created DESC"
|
||||
)
|
||||
.bind(status)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find all paused workflow executions
|
||||
pub async fn find_paused<'e, E>(executor: E) -> Result<Vec<WorkflowExecution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
WHERE paused = true
|
||||
ORDER BY created DESC"
|
||||
)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Find workflow executions by workflow definition
|
||||
pub async fn find_by_workflow_def<'e, E>(
|
||||
executor: E,
|
||||
workflow_def_id: Id,
|
||||
) -> Result<Vec<WorkflowExecution>>
|
||||
where
|
||||
E: Executor<'e, Database = Postgres> + 'e,
|
||||
{
|
||||
sqlx::query_as::<_, WorkflowExecution>(
|
||||
"SELECT id, execution, workflow_def, current_tasks, completed_tasks, failed_tasks, skipped_tasks,
|
||||
variables, task_graph, status, error_message, paused, pause_reason, created, updated
|
||||
FROM workflow_execution
|
||||
WHERE workflow_def = $1
|
||||
ORDER BY created DESC"
|
||||
)
|
||||
.bind(workflow_def_id)
|
||||
.fetch_all(executor)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user