Skip to main content

aro_fletch/
repo.rs

1//! Generic fletch-backed repository adapter.
2
3use std::marker::PhantomData;
4use std::sync::Arc;
5
6use aro_core::error::RepoError;
7use aro_core::repository::{BoxFuture, Repository};
8use aro_web::dep::Dep;
9use sqlx::Database;
10
11use crate::error::from_fletch_err;
12
13/// A SQLx database backend supported by aro-fletch.
14///
15/// Enabled via Cargo features: `sqlite`, `postgres`, `mysql`.
16pub trait FletchDatabase: Database + Send + Sync + 'static {}
17
18#[cfg(feature = "sqlite")]
19impl FletchDatabase for sqlx::Sqlite {}
20
21#[cfg(feature = "postgres")]
22impl FletchDatabase for sqlx::Postgres {}
23
24#[cfg(feature = "mysql")]
25impl FletchDatabase for sqlx::MySql {}
26
27/// A fletch entity suitable for the repository adapter.
28///
29/// This trait combines `aro_core::Entity` with `fletch_orm::Entity` and the
30/// `sqlx::FromRow` bound required by fletch's query layer.
31pub trait FletchEntity<DB: FletchDatabase>:
32    aro_core::entity::Entity
33    + fletch_orm::Entity<Id = <Self as aro_core::entity::Entity>::Id>
34    + for<'r> sqlx::FromRow<'r, DB::Row>
35    + Send
36    + Sync
37    + Unpin
38    + Clone
39{
40}
41
42/// Blanket implementation for any type meeting all bounds.
43impl<T, DB> FletchEntity<DB> for T
44where
45    DB: FletchDatabase,
46    T: aro_core::entity::Entity
47        + fletch_orm::Entity<Id = <T as aro_core::entity::Entity>::Id>
48        + for<'r> sqlx::FromRow<'r, DB::Row>
49        + Send
50        + Sync
51        + Unpin
52        + Clone,
53{
54}
55
56/// Generic fletch-backed repository for any domain entity implementing [`FletchEntity`].
57///
58/// All CRUD operations run against the pool held by this repository.
59pub struct FletchRepository<T, DB: FletchDatabase> {
60    pool: fletch_orm::Pool<DB>,
61    _marker: PhantomData<T>,
62}
63
64impl<T, DB> FletchRepository<T, DB>
65where
66    DB: FletchDatabase,
67{
68    /// Create a new repository backed by the given pool.
69    pub fn new(pool: fletch_orm::Pool<DB>) -> Self {
70        Self {
71            pool,
72            _marker: PhantomData,
73        }
74    }
75
76    /// Create a new repository by cloning the pool from a [`Dep`].
77    ///
78    /// `fletch_orm::Pool` wraps an `sqlx::Pool` internally, so cloning is
79    /// cheap and all clones share the same pool.
80    pub fn from_dep(dep: &Dep<fletch_orm::Pool<DB>>) -> Self {
81        Self::new((**dep).clone())
82    }
83
84    /// Return a reference to the underlying connection pool.
85    pub fn pool(&self) -> &fletch_orm::Pool<DB> {
86        &self.pool
87    }
88}
89
90/// Transaction-scoped repository adapter delegating to fletch CRUD on the open transaction.
91///
92/// Created from [`FletchTransactionContext::repo`](crate::transaction::FletchTransactionContext::repo)
93/// so writes participate in the open transaction and roll back on failure.
94pub struct FletchTransactionalRepository<T, DB: FletchDatabase> {
95    state: Arc<crate::transaction::FletchTransactionState<DB>>,
96    _marker: PhantomData<T>,
97}
98
99impl<T, DB> FletchTransactionalRepository<T, DB>
100where
101    DB: FletchDatabase,
102{
103    pub(crate) fn new(state: Arc<crate::transaction::FletchTransactionState<DB>>) -> Self {
104        Self {
105            state,
106            _marker: PhantomData,
107        }
108    }
109}
110
111/*
112 * Transactional create/update differ by backend:
113 *
114 * - `returning` (SQLite, Postgres): use `fletch_orm::insert` / `fletch_orm::update` with
115 *   RETURNING on the open connection.
116 * - `mysql`: use [`fletch_orm::Transaction::insert`] / [`fletch_orm::Transaction::update`]
117 *   because MySQL has no RETURNING and re-selects via last insert id.
118 */
119macro_rules! impl_transactional_repository {
120    (mysql, $db:ty, $dialect:expr) => {
121        impl<T> FletchTransactionalRepository<T, $db>
122        where
123            T: FletchEntity<$db>,
124        {
125            /// Find an entity by its unique identifier.
126            pub async fn find_by_id(
127                &self,
128                id: <T as aro_core::entity::Entity>::Id,
129            ) -> Result<T, RepoError> {
130                let mut guard = self.state.tx.lock().await;
131                let tx = guard
132                    .as_mut()
133                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
134                fletch_orm::find_by_id($dialect, &mut **tx.inner_mut(), id)
135                    .await
136                    .map_err(from_fletch_err)
137            }
138
139            /// Return all entities of this type.
140            pub async fn find_all(&self) -> Result<Vec<T>, RepoError> {
141                let mut guard = self.state.tx.lock().await;
142                let tx = guard
143                    .as_mut()
144                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
145                fletch_orm::find_all($dialect, &mut **tx.inner_mut())
146                    .await
147                    .map_err(from_fletch_err)
148            }
149
150            /// Persist a new entity and return the created value.
151            pub async fn create(&self, entity: T) -> Result<T, RepoError> {
152                let mut guard = self.state.tx.lock().await;
153                let tx = guard
154                    .as_mut()
155                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
156                tx.insert(&entity).await.map_err(from_fletch_err)
157            }
158
159            /// Update an existing entity and return the updated value.
160            pub async fn update(&self, entity: T) -> Result<T, RepoError> {
161                let mut guard = self.state.tx.lock().await;
162                let tx = guard
163                    .as_mut()
164                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
165                tx.update(&entity).await.map_err(from_fletch_err)
166            }
167
168            /// Delete an entity by its unique identifier.
169            pub async fn delete(
170                &self,
171                id: <T as aro_core::entity::Entity>::Id,
172            ) -> Result<(), RepoError> {
173                let mut guard = self.state.tx.lock().await;
174                let tx = guard
175                    .as_mut()
176                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
177                fletch_orm::delete::<T, $db, _, _>($dialect, &mut **tx.inner_mut(), id)
178                    .await
179                    .map_err(from_fletch_err)
180            }
181        }
182    };
183
184    (returning, $db:ty, $dialect:expr) => {
185        impl<T> FletchTransactionalRepository<T, $db>
186        where
187            T: FletchEntity<$db>,
188        {
189            /// Find an entity by its unique identifier.
190            pub async fn find_by_id(
191                &self,
192                id: <T as aro_core::entity::Entity>::Id,
193            ) -> Result<T, RepoError> {
194                let mut guard = self.state.tx.lock().await;
195                let tx = guard
196                    .as_mut()
197                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
198                fletch_orm::find_by_id($dialect, &mut **tx.inner_mut(), id)
199                    .await
200                    .map_err(from_fletch_err)
201            }
202
203            /// Return all entities of this type.
204            pub async fn find_all(&self) -> Result<Vec<T>, RepoError> {
205                let mut guard = self.state.tx.lock().await;
206                let tx = guard
207                    .as_mut()
208                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
209                fletch_orm::find_all($dialect, &mut **tx.inner_mut())
210                    .await
211                    .map_err(from_fletch_err)
212            }
213
214            /// Persist a new entity and return the created value.
215            pub async fn create(&self, entity: T) -> Result<T, RepoError> {
216                let mut guard = self.state.tx.lock().await;
217                let tx = guard
218                    .as_mut()
219                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
220                fletch_orm::insert($dialect, &mut **tx.inner_mut(), &entity)
221                    .await
222                    .map_err(from_fletch_err)
223            }
224
225            /// Update an existing entity and return the updated value.
226            pub async fn update(&self, entity: T) -> Result<T, RepoError> {
227                let mut guard = self.state.tx.lock().await;
228                let tx = guard
229                    .as_mut()
230                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
231                fletch_orm::update($dialect, &mut **tx.inner_mut(), &entity)
232                    .await
233                    .map_err(from_fletch_err)
234            }
235
236            /// Delete an entity by its unique identifier.
237            pub async fn delete(
238                &self,
239                id: <T as aro_core::entity::Entity>::Id,
240            ) -> Result<(), RepoError> {
241                let mut guard = self.state.tx.lock().await;
242                let tx = guard
243                    .as_mut()
244                    .ok_or_else(|| RepoError::Unknown("transaction is no longer active".into()))?;
245                fletch_orm::delete::<T, $db, _, _>($dialect, &mut **tx.inner_mut(), id)
246                    .await
247                    .map_err(from_fletch_err)
248            }
249        }
250    };
251}
252
253#[cfg(feature = "sqlite")]
254impl_transactional_repository!(returning, sqlx::Sqlite, &fletch_orm::SqliteDialect);
255
256#[cfg(feature = "postgres")]
257impl_transactional_repository!(returning, sqlx::Postgres, &fletch_orm::PostgresDialect);
258
259#[cfg(feature = "mysql")]
260impl_transactional_repository!(mysql, sqlx::MySql, &fletch_orm::MySqlDialect);
261
262macro_rules! impl_repository {
263    ($db:ty) => {
264        impl<T> Repository<T> for FletchRepository<T, $db>
265        where
266            T: FletchEntity<$db>,
267            <T as aro_core::entity::Entity>::Id: Into<fletch_orm::Value>,
268        {
269            fn find_by_id(
270                &self,
271                id: <T as aro_core::entity::Entity>::Id,
272            ) -> BoxFuture<'_, Result<T, RepoError>> {
273                Box::pin(
274                    async move { self.pool.find_by_id::<T>(id).await.map_err(from_fletch_err) },
275                )
276            }
277
278            fn find_all(&self) -> BoxFuture<'_, Result<Vec<T>, RepoError>> {
279                Box::pin(async move { self.pool.find_all::<T>().await.map_err(from_fletch_err) })
280            }
281
282            fn create(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>> {
283                Box::pin(async move {
284                    self.pool
285                        .insert::<T>(&entity)
286                        .await
287                        .map_err(from_fletch_err)
288                })
289            }
290
291            fn update(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>> {
292                Box::pin(async move {
293                    self.pool
294                        .update::<T>(&entity)
295                        .await
296                        .map_err(from_fletch_err)
297                })
298            }
299
300            fn delete(
301                &self,
302                id: <T as aro_core::entity::Entity>::Id,
303            ) -> BoxFuture<'_, Result<(), RepoError>> {
304                Box::pin(async move { self.pool.delete::<T>(id).await.map_err(from_fletch_err) })
305            }
306        }
307    };
308}
309
310#[cfg(feature = "sqlite")]
311impl_repository!(sqlx::Sqlite);
312
313#[cfg(feature = "postgres")]
314impl_repository!(sqlx::Postgres);
315
316#[cfg(feature = "mysql")]
317impl_repository!(sqlx::MySql);