Files
sustenance/src/ports.rs
T
sbstp 689bd95ff0
ci/woodpecker/push/e2e Pipeline was successful
ci/woodpecker/push/fmt Pipeline failed
ci/woodpecker/push/test Pipeline was successful
list archive
2026-08-07 22:16:29 -04:00

317 lines
8.7 KiB
Rust

use async_trait::async_trait;
use sqlx::SqliteConnection;
use crate::domain::{
Category, DomainResult, GroceryList, Item, ListMeal, Meal, MealCategory, MealIngredient,
Passkey, 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 find_user_by_handle(
&self,
txn: &mut SqliteConnection,
user_handle: Vec<u8>,
) -> DomainResult<Option<User>>;
async fn update_password_hash(
&self,
txn: &mut SqliteConnection,
user_id: i64,
password_hash: String,
) -> DomainResult<()>;
async fn has_users(&self, txn: &mut SqliteConnection) -> DomainResult<bool>;
}
#[async_trait]
pub trait PasskeyRepository: Send + Sync {
async fn create_passkey(
&self,
txn: &mut SqliteConnection,
user_id: i64,
credential_id: String,
credential: String,
counter: i64,
) -> DomainResult<Passkey>;
async fn find_by_credential_id(
&self,
txn: &mut SqliteConnection,
credential_id: String,
) -> DomainResult<Option<Passkey>>;
async fn list_for_user(
&self,
txn: &mut SqliteConnection,
user_id: i64,
) -> DomainResult<Vec<Passkey>>;
async fn delete_passkey(
&self,
txn: &mut SqliteConnection,
user_id: i64,
passkey_id: i64,
) -> DomainResult<()>;
}
#[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 list_archived_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 fn set_archived(
&self,
txn: &mut SqliteConnection,
list_id: i64,
archived: bool,
) -> DomainResult<()>;
}
#[async_trait]
pub trait CategoryRepository: Send + Sync {
async fn categories(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<Category>>;
async fn create_category(&self, txn: &mut SqliteConnection, name: String) -> DomainResult<i64>;
}
/// A single item to insert in bulk, without a per-item revision bump.
#[derive(Clone, Debug)]
pub struct NewItem {
pub name: String,
pub quantity: String,
pub note: String,
pub category_id: Option<i64>,
/// When set, links this item to the `list_meals` row it came from, so the
/// item is removed together with that meal instance.
pub list_meal_id: Option<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 add_items_bulk(
&self,
txn: &mut SqliteConnection,
list_id: i64,
items: Vec<NewItem>,
) -> 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 ListMealRepository: Send + Sync {
async fn list_meals(
&self,
txn: &mut SqliteConnection,
list_id: i64,
) -> DomainResult<Vec<ListMeal>>;
async fn add_meal(
&self,
txn: &mut SqliteConnection,
list_id: i64,
meal_id: i64,
name: String,
) -> DomainResult<i64>;
async fn remove_meal(
&self,
txn: &mut SqliteConnection,
list_id: i64,
list_meal_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 MealCategoryRepository: Send + Sync {
async fn meal_categories(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<MealCategory>>;
async fn create_meal_category(
&self,
txn: &mut SqliteConnection,
name: String,
) -> DomainResult<i64>;
async fn delete_meal_category(
&self,
txn: &mut SqliteConnection,
category_id: i64,
) -> DomainResult<()>;
}
#[async_trait]
pub trait MealRepository: Send + Sync {
async fn create_meal(
&self,
txn: &mut SqliteConnection,
name: String,
description: String,
category_id: Option<i64>,
) -> DomainResult<Meal>;
async fn get_meal(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
) -> DomainResult<Option<Meal>>;
async fn list_meals(&self, txn: &mut SqliteConnection) -> DomainResult<Vec<Meal>>;
async fn update_meal(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
name: String,
description: String,
category_id: Option<i64>,
) -> DomainResult<()>;
async fn delete_meal(&self, txn: &mut SqliteConnection, meal_id: i64) -> DomainResult<()>;
}
#[async_trait]
pub trait MealIngredientRepository: Send + Sync {
async fn ingredients_for_meal(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
) -> DomainResult<Vec<MealIngredient>>;
async fn add_ingredient(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<i64>;
async fn update_ingredient(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
ingredient_id: i64,
name: String,
quantity: String,
note: String,
category_id: Option<i64>,
) -> DomainResult<()>;
async fn delete_ingredient(
&self,
txn: &mut SqliteConnection,
meal_id: i64,
ingredient_id: i64,
) -> DomainResult<()>;
}
#[async_trait]
pub trait PasswordHasher: Send + Sync {
fn hash(&self, password: &str) -> DomainResult<String>;
fn verify(&self, password: &str, encoded_hash: &str) -> DomainResult<bool>;
}
#[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 },
}