Files
attune/crates/common/src/repositories/mod.rs
David Culbreth f93e9229d2
Some checks failed
CI / Rustfmt (pull_request) Successful in 19s
CI / Cargo Audit & Deny (pull_request) Successful in 33s
CI / Security Blocking Checks (pull_request) Successful in 5s
CI / Web Blocking Checks (pull_request) Successful in 49s
CI / Web Advisory Checks (pull_request) Successful in 33s
CI / Clippy (pull_request) Has been cancelled
CI / Security Advisory Checks (pull_request) Has been cancelled
CI / Tests (pull_request) Has been cancelled
ha executor
2026-04-02 17:15:59 -05:00

321 lines
9.2 KiB
Rust

//! 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 analytics;
pub mod artifact;
pub mod entity_history;
pub mod event;
pub mod execution;
pub mod execution_admission;
pub mod identity;
pub mod inquiry;
pub mod key;
pub mod notification;
pub mod pack;
pub mod pack_test;
pub mod queue_stats;
pub mod rule;
pub mod runtime;
pub mod runtime_version;
pub mod trigger;
pub mod workflow;
// Re-export repository types
pub use action::{ActionRepository, PolicyRepository};
pub use analytics::AnalyticsRepository;
pub use artifact::{ArtifactRepository, ArtifactVersionRepository};
pub use entity_history::EntityHistoryRepository;
pub use event::{EnforcementRepository, EventRepository};
pub use execution::ExecutionRepository;
pub use execution_admission::ExecutionAdmissionRepository;
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_test::PackTestRepository;
pub use queue_stats::QueueStatsRepository;
pub use rule::RuleRepository;
pub use runtime::{RuntimeRepository, WorkerRepository};
pub use runtime_version::RuntimeVersionRepository;
pub use trigger::{SensorRepository, TriggerRepository};
pub use workflow::{WorkflowDefinitionRepository, WorkflowExecutionRepository};
/// Explicit patch operation for update inputs where callers must distinguish
/// between "leave unchanged", "set value", and "clear to NULL".
#[derive(Debug, Clone, PartialEq)]
pub enum Patch<T> {
Set(T),
Clear,
}
/// 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);
}
}