brewlog/src/domain/repositories.rs
Jon Seager b794d8a63f
feat(domain): add Gear entity with database migrations
Add Gear entity to track brewing equipment (grinders and brewers) with complete domain layer implementation.

Database changes:
- migrations/0006_add_gear.sql: Create gear table with category CHECK constraint and indexes
- migrations/0007_update_timeline_for_gear.sql: Document 'gear' as valid timeline entity type

Domain layer:
- Add GearId typed ID wrapper
- Create domain/gear.rs with:
  - GearCategory enum (Grinder/Brewer) with string conversion methods
  - Gear entity with make, model, notes fields
  - NewGear and UpdateGear DTOs
  - GearFilter for category-based filtering
  - GearSortKey with Make (default), Model, Category, CreatedAt options
- Add GearRepository trait to domain/repositories.rs with standard CRUD operations
- Register gear module in domain/mod.rs

This follows the same architectural pattern as the Bag entity.
2026-02-02 15:14:23 +00:00

148 lines
6.1 KiB
Rust

use super::RepositoryError;
use crate::domain::listing::{ListRequest, Page, SortDirection, SortKey};
use crate::domain::bags::{Bag, BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
use crate::domain::gear::{Gear, GearFilter, GearSortKey, NewGear, UpdateGear};
use crate::domain::ids::{BagId, GearId, RoastId, RoasterId, SessionId, TokenId, UserId};
use crate::domain::roasters::RoasterSortKey;
use crate::domain::roasters::{NewRoaster, Roaster, UpdateRoaster};
use crate::domain::roasts::RoastSortKey;
use crate::domain::roasts::{NewRoast, Roast, RoastWithRoaster, UpdateRoast};
use crate::domain::sessions::{NewSession, Session};
use crate::domain::timeline::{NewTimelineEvent, TimelineEvent, TimelineSortKey};
use crate::domain::tokens::{NewToken, Token};
use crate::domain::users::{NewUser, User};
use async_trait::async_trait;
#[async_trait]
pub trait RoasterRepository: Send + Sync {
async fn insert(&self, roaster: NewRoaster) -> Result<Roaster, RepositoryError>;
async fn get(&self, id: RoasterId) -> Result<Roaster, RepositoryError>;
async fn get_by_slug(&self, slug: &str) -> Result<Roaster, RepositoryError>;
async fn list(
&self,
request: &ListRequest<RoasterSortKey>,
) -> Result<Page<Roaster>, RepositoryError>;
async fn update(
&self,
id: RoasterId,
changes: UpdateRoaster,
) -> Result<Roaster, RepositoryError>;
async fn delete(&self, id: RoasterId) -> Result<(), RepositoryError>;
async fn list_all(&self) -> Result<Vec<Roaster>, RepositoryError> {
let sort_key = <RoasterSortKey as SortKey>::default();
let request =
ListRequest::<RoasterSortKey>::show_all(sort_key, sort_key.default_direction());
let page = self.list(&request).await?;
Ok(page.items)
}
async fn list_all_sorted(
&self,
sort_key: RoasterSortKey,
direction: SortDirection,
) -> Result<Vec<Roaster>, RepositoryError> {
let request = ListRequest::show_all(sort_key, direction);
let page = self.list(&request).await?;
Ok(page.items)
}
}
#[async_trait]
pub trait RoastRepository: Send + Sync {
async fn insert(&self, roast: NewRoast) -> Result<Roast, RepositoryError>;
async fn get(&self, id: RoastId) -> Result<Roast, RepositoryError>;
async fn get_by_slug(
&self,
roaster_id: RoasterId,
slug: &str,
) -> Result<Roast, RepositoryError>;
async fn list(
&self,
request: &ListRequest<RoastSortKey>,
) -> Result<Page<RoastWithRoaster>, RepositoryError>;
async fn list_by_roaster(
&self,
roaster_id: RoasterId,
) -> Result<Vec<RoastWithRoaster>, RepositoryError>;
async fn update(&self, id: RoastId, changes: UpdateRoast) -> Result<Roast, RepositoryError>;
async fn delete(&self, id: RoastId) -> Result<(), RepositoryError>;
async fn list_all(&self) -> Result<Vec<RoastWithRoaster>, RepositoryError> {
let sort_key = <RoastSortKey as SortKey>::default();
let request = ListRequest::<RoastSortKey>::show_all(sort_key, sort_key.default_direction());
let page = self.list(&request).await?;
Ok(page.items)
}
}
#[async_trait]
pub trait TimelineEventRepository: Send + Sync {
async fn insert(&self, event: NewTimelineEvent) -> Result<TimelineEvent, RepositoryError>;
async fn list(
&self,
request: &ListRequest<TimelineSortKey>,
) -> Result<Page<TimelineEvent>, RepositoryError>;
async fn list_all(&self) -> Result<Vec<TimelineEvent>, RepositoryError> {
let sort_key = <TimelineSortKey as SortKey>::default();
let request =
ListRequest::<TimelineSortKey>::show_all(sort_key, sort_key.default_direction());
let page = self.list(&request).await?;
Ok(page.items)
}
}
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn insert(&self, user: NewUser) -> Result<User, RepositoryError>;
async fn get(&self, id: UserId) -> Result<User, RepositoryError>;
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError>;
async fn exists(&self) -> Result<bool, RepositoryError>;
}
#[async_trait]
pub trait TokenRepository: Send + Sync {
async fn insert(&self, token: NewToken) -> Result<Token, RepositoryError>;
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError>;
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError>;
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError>;
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError>;
async fn update_last_used(&self, id: TokenId) -> Result<(), RepositoryError>;
}
#[async_trait]
pub trait SessionRepository: Send + Sync {
async fn insert(&self, session: NewSession) -> Result<Session, RepositoryError>;
async fn get(&self, id: SessionId) -> Result<Session, RepositoryError>;
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Session, RepositoryError>;
async fn delete(&self, id: SessionId) -> Result<(), RepositoryError>;
async fn delete_expired(&self) -> Result<(), RepositoryError>;
}
#[async_trait]
pub trait BagRepository: Send + Sync {
async fn insert(&self, bag: NewBag) -> Result<Bag, RepositoryError>;
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError>;
async fn list(
&self,
filter: BagFilter,
request: &ListRequest<BagSortKey>,
) -> Result<Page<BagWithRoast>, RepositoryError>;
async fn update(&self, id: BagId, changes: UpdateBag) -> Result<Bag, RepositoryError>;
async fn delete(&self, id: BagId) -> Result<(), RepositoryError>;
}
#[async_trait]
pub trait GearRepository: Send + Sync {
async fn insert(&self, gear: NewGear) -> Result<Gear, RepositoryError>;
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError>;
async fn list(
&self,
filter: GearFilter,
request: &ListRequest<GearSortKey>,
) -> Result<Page<Gear>, RepositoryError>;
async fn update(&self, id: GearId, changes: UpdateGear) -> Result<Gear, RepositoryError>;
async fn delete(&self, id: GearId) -> Result<(), RepositoryError>;
}