hexagonal refactor

This commit is contained in:
2026-08-01 18:27:22 -04:00
parent 6979cabe8b
commit 188ce23e67
13 changed files with 3590 additions and 1795 deletions
+158
View File
@@ -0,0 +1,158 @@
use async_trait::async_trait;
use sqlx::SqliteConnection;
use crate::domain::{Category, DomainResult, GroceryList, Item, PresenceUser, SessionUser, User};
/// Repositories take `&mut SqliteConnection` (which a `Transaction` derefs to),
/// so several repositories can commit together atomically within a single
/// transaction coordinated by the unit of work.
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn create_user(
&self,
txn: &mut SqliteConnection,
email: String,
display_name: String,
password_hash: String,
) -> DomainResult<User>;
async fn find_user_by_email(
&self,
txn: &mut SqliteConnection,
email: String,
) -> DomainResult<Option<(User, String)>>;
async fn has_users(&self, txn: &mut SqliteConnection) -> DomainResult<bool>;
}
#[async_trait]
pub trait SessionRepository: Send + Sync {
async fn create_session(
&self,
txn: &mut SqliteConnection,
user_id: i64,
) -> DomainResult<(String, String)>;
async fn session_user(
&self,
txn: &mut SqliteConnection,
session_token: String,
) -> DomainResult<Option<SessionUser>>;
async fn delete_session(
&self,
txn: &mut SqliteConnection,
session_token: String,
) -> DomainResult<()>;
}
#[async_trait]
pub trait ListRepository: Send + Sync {
async fn list_summaries(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<GroceryList>>;
async fn create_list(
&self,
txn: &mut SqliteConnection,
name: String,
) -> DomainResult<GroceryList>;
async fn get_list(
&self,
txn: &mut SqliteConnection,
list_id: i64,
) -> DomainResult<Option<GroceryList>>;
}
#[async_trait]
pub trait CategoryRepository: Send + Sync {
async fn categories(
&self,
txn: &mut SqliteConnection,
list_id: i64,
) -> DomainResult<Vec<Category>>;
async fn create_category(
&self,
txn: &mut SqliteConnection,
list_id: i64,
name: String,
) -> DomainResult<i64>;
}
#[async_trait]
pub trait ItemRepository: Send + Sync {
async fn items(&self, txn: &mut SqliteConnection, list_id: i64) -> DomainResult<Vec<Item>>;
async fn add_item(
&self,
txn: &mut SqliteConnection,
list_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<i64>;
async fn set_item_checked(
&self,
txn: &mut SqliteConnection,
list_id: i64,
item_id: i64,
checked: bool,
) -> DomainResult<i64>;
async fn update_item(
&self,
txn: &mut SqliteConnection,
list_id: i64,
item_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<i64>;
async fn delete_item(
&self,
txn: &mut SqliteConnection,
list_id: i64,
item_id: i64,
) -> DomainResult<i64>;
}
#[async_trait]
pub trait InvitationRepository: Send + Sync {
async fn create_invitation(
&self,
txn: &mut SqliteConnection,
created_by: i64,
token: String,
) -> DomainResult<i64>;
async fn invitation(&self, txn: &mut SqliteConnection, token: String) -> DomainResult<bool>;
async fn accept_invitation(
&self,
txn: &mut SqliteConnection,
token: String,
) -> DomainResult<()>;
}
#[async_trait]
pub trait PasswordHasher: Send + Sync {
fn hash(&self, password: &str) -> Result<String, String>;
fn verify(&self, password: &str, encoded_hash: &str) -> Result<bool, String>;
}
#[async_trait]
pub trait TokenGenerator: Send + Sync {
fn generate(&self) -> String;
}
#[async_trait]
pub trait RealtimeNotifier: Send + Sync {
async fn join(&self, list_id: i64, user_id: i64, display_name: String) -> Subscription;
async fn leave(&self, list_id: i64, connection_id: &str);
async fn publish_list_changed(&self, list_id: i64, revision: i64);
async fn presence(&self, list_id: i64) -> Vec<PresenceUser>;
}
pub struct Subscription {
pub connection_id: String,
pub receiver: tokio::sync::broadcast::Receiver<HubEvent>,
pub presence: Vec<PresenceUser>,
}
#[derive(Clone, Debug)]
pub enum HubEvent {
ListChanged { list_id: i64, revision: i64 },
PresenceChanged { list_id: i64 },
}