re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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);
}
}

View 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)
}
}

View 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());
}
}

View 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
}

View 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);
}
*/
}

View 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);
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}