axum_gate/accounts/account_repository.rs
1use crate::errors::Result;
2use crate::{accounts::Account, authz::AccessHierarchy};
3
4use std::future::Future;
5
6/// Repository abstraction for persisting and retrieving [`Account`] entities.
7///
8/// This trait is implemented by storage backends (e.g. in‑memory, SurrealDB, SeaORM).
9/// It deliberately uses an `Option<Account<..>>` in results for operations where
10/// absence is a normal outcome (delete / update / query) so callers can
11/// distinguish “not found” from actual errors (`Result::Err`).
12///
13/// # Semantics
14///
15/// | Method | Success (`Ok`) Return Value | Typical `None` Meaning | Error (`Err`) Meaning |
16/// |-----------------------|----------------------------------------------------|-------------------------------------------|--------------------------------------------------|
17/// | `store_account` | `Some(Account)` if stored | `None` only if backend chooses (rare) | Persistence / connectivity / constraint failure |
18/// | `delete_account` | `Some(Account)` = deleted & returned | `None` = no account with that user id | Backend / IO failure |
19/// | `update_account` | `Some(Account)` = updated | `None` = no existing account to update | Backend / IO / optimistic concurrency failure |
20/// | `query_account_by_user_id` | `Some(Account)` = found | `None` = not found | Backend / IO failure |
21///
22/// Backends SHOULD:
23/// - Treat `user_id` as a logical unique key
24/// - Enforce uniqueness at storage level where possible
25/// - Return **identical timing characteristics** for “found” vs “not found” where feasible
26/// (helps upstream login logic resist user enumeration timing attacks)
27///
28/// # Concurrency & Consistency
29///
30/// This trait does not prescribe isolation semantics. Implementations should document:
31/// - Whether updates are last‑write‑wins
32/// - Whether optimistic locking / versioning is applied
33///
34/// # Example (generic usage)
35/// ```rust
36///
37/// use axum_gate::accounts::Account;
38/// use axum_gate::prelude::{Role, Group};
39/// use axum_gate::accounts::AccountRepository;
40/// use axum_gate::repositories::memory::MemoryAccountRepository;
41///
42/// # #[tokio::test]
43/// async fn load_or_create(
44/// repo: &MemoryAccountRepository<Role, Group>,
45/// template: Account<Role, Group>
46/// ) -> axum_gate::errors::Result<Account<Role, Group>> {
47/// if let Some(existing) = repo.query_account_by_user_id(&template.user_id).await? {
48/// Ok(existing)
49/// } else {
50/// Ok(repo.store_account(template).await?.expect("store returned None"))
51/// }
52/// }
53/// ```
54///
55/// # Error Handling
56///
57/// Return `Err` only for exceptional backend failures (connectivity, serialization,
58/// constraint violation, etc.). Use `Ok(None)` for “not found” / “no-op” outcomes.
59///
60/// # Extensibility
61///
62/// If you add methods (e.g. pagination, search), prefer separate traits to avoid forcing
63/// all backends to implement optional features.
64pub trait AccountRepository<R, G>
65where
66 Self: Send + Sync,
67 R: AccessHierarchy + Eq,
68 G: Eq + Clone,
69{
70 /// Persist a new account.
71 ///
72 /// Implementations SHOULD enforce uniqueness of `user_id`. Returning `Ok(Some(account))`
73 /// indicates success. Returning `Ok(None)` is discouraged unless there is a documented
74 /// race / conditional insert semantics the backend wishes to expose.
75 fn store_account(
76 &self,
77 account: Account<R, G>,
78 ) -> impl Future<Output = Result<Option<Account<R, G>>>> + Send;
79
80 /// Delete an account identified by its `user_id`.
81 ///
82 /// Returns:
83 /// - `Ok(Some(account))` if the account existed and was removed
84 /// - `Ok(None)` if no account matched `user_id`
85 /// - `Err(e)` on backend error
86 fn delete_account(
87 &self,
88 user_id: &str,
89 ) -> impl Future<Output = Result<Option<Account<R, G>>>> + Send;
90
91 /// Update an existing account.
92 ///
93 /// Implementations may perform either full replacement or partial persistence depending
94 /// on backend capabilities (document if non‑standard). Returns:
95 /// - `Ok(Some(updated_account))` on success
96 /// - `Ok(None)` if the account does not exist
97 /// - `Err(e)` on failure
98 fn update_account(
99 &self,
100 account: Account<R, G>,
101 ) -> impl Future<Output = Result<Option<Account<R, G>>>> + Send;
102
103 /// Fetch an account by its logical user identifier.
104 ///
105 /// This must **not** leak timing differences exploitable for enumeration if used
106 /// together with authentication flows relying on indistinguishable “not found”.
107 ///
108 /// Returns:
109 /// - `Ok(Some(account))` if found
110 /// - `Ok(None)` if not found
111 /// - `Err(e)` on backend failure
112 fn query_account_by_user_id(
113 &self,
114 user_id: &str,
115 ) -> impl Future<Output = Result<Option<Account<R, G>>>> + Send;
116}