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 R: AccessHierarchy + Eq,
67 G: Eq + Clone,
68{
69 /// Persist a new account.
70 ///
71 /// Implementations SHOULD enforce uniqueness of `user_id`. Returning `Ok(Some(account))`
72 /// indicates success. Returning `Ok(None)` is discouraged unless there is a documented
73 /// race / conditional insert semantics the backend wishes to expose.
74 fn store_account(
75 &self,
76 account: Account<R, G>,
77 ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
78
79 /// Delete an account identified by its `user_id`.
80 ///
81 /// Returns:
82 /// - `Ok(Some(account))` if the account existed and was removed
83 /// - `Ok(None)` if no account matched `user_id`
84 /// - `Err(e)` on backend error
85 fn delete_account(&self, user_id: &str) -> impl Future<Output = Result<Option<Account<R, G>>>>;
86
87 /// Update an existing account.
88 ///
89 /// Implementations may perform either full replacement or partial persistence depending
90 /// on backend capabilities (document if non‑standard). Returns:
91 /// - `Ok(Some(updated_account))` on success
92 /// - `Ok(None)` if the account does not exist
93 /// - `Err(e)` on failure
94 fn update_account(
95 &self,
96 account: Account<R, G>,
97 ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
98
99 /// Fetch an account by its logical user identifier.
100 ///
101 /// This must **not** leak timing differences exploitable for enumeration if used
102 /// together with authentication flows relying on indistinguishable “not found”.
103 ///
104 /// Returns:
105 /// - `Ok(Some(account))` if found
106 /// - `Ok(None)` if not found
107 /// - `Err(e)` on backend failure
108 fn query_account_by_user_id(
109 &self,
110 user_id: &str,
111 ) -> impl Future<Output = Result<Option<Account<R, G>>>>;
112}