ferro_rs/database/model.rs
1//! Model traits for Ferro ORM
2//!
3//! Provides Laravel-like active record pattern over SeaORM entities.
4//!
5//! # Scoped Queries
6//!
7//! Use the `ScopedQuery` trait to define reusable query scopes:
8//!
9//! ```rust,ignore
10//! impl ScopedQuery for animals::Entity {
11//! type Scope = AnimalScope;
12//! }
13//!
14//! enum AnimalScope {
15//! ForShelter(i64),
16//! Available,
17//! Species(String),
18//! }
19//!
20//! // Usage:
21//! let animals = Animal::scoped(AnimalScope::ForShelter(shelter_id))
22//! .and(AnimalScope::Available)
23//! .all()
24//! .await?;
25//! ```
26
27use async_trait::async_trait;
28use sea_orm::{
29 ActiveModelBehavior, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait,
30 PaginatorTrait, PrimaryKeyTrait, TryIntoModel,
31};
32
33use crate::database::{QueryBuilder, DB};
34use crate::error::FrameworkError;
35
36/// Trait providing Laravel-like read operations on SeaORM entities
37///
38/// Implement this trait on your SeaORM Entity to get convenient static methods
39/// for querying records.
40///
41/// # Example
42///
43/// ```rust,ignore
44/// use ferro_rs::database::Model;
45/// use sea_orm::entity::prelude::*;
46///
47/// #[derive(Clone, Debug, DeriveEntityModel)]
48/// #[sea_orm(table_name = "users")]
49/// pub struct Model {
50/// #[sea_orm(primary_key)]
51/// pub id: i32,
52/// pub name: String,
53/// pub email: String,
54/// }
55///
56/// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
57/// pub enum Relation {}
58///
59/// impl ActiveModelBehavior for ActiveModel {}
60///
61/// // Add Ferro's Model trait
62/// impl ferro_rs::database::Model for Entity {}
63///
64/// // Now you can use:
65/// let users = Entity::all().await?;
66/// let user = Entity::find_by_pk(1).await?;
67/// ```
68#[async_trait]
69pub trait Model: EntityTrait + Sized
70where
71 Self::Model: ModelTrait<Entity = Self> + Send + Sync,
72{
73 /// Find all records
74 ///
75 /// # Example
76 /// ```rust,ignore
77 /// let users = user::Entity::all().await?;
78 /// ```
79 async fn all() -> Result<Vec<Self::Model>, FrameworkError> {
80 let db = DB::connection()?;
81 Self::find()
82 .all(db.inner())
83 .await
84 .map_err(|e| FrameworkError::database(e.to_string()))
85 }
86
87 /// Find a record by primary key (generic version)
88 ///
89 /// # Example
90 /// ```rust,ignore
91 /// let user = user::Entity::find_by_pk(1).await?;
92 /// ```
93 async fn find_by_pk<K>(id: K) -> Result<Option<Self::Model>, FrameworkError>
94 where
95 K: Into<<Self::PrimaryKey as PrimaryKeyTrait>::ValueType> + Send,
96 {
97 let db = DB::connection()?;
98 Self::find_by_id(id)
99 .one(db.inner())
100 .await
101 .map_err(|e| FrameworkError::database(e.to_string()))
102 }
103
104 /// Find a record by primary key or return an error
105 ///
106 /// # Example
107 /// ```rust,ignore
108 /// let user = user::Entity::find_or_fail(1).await?;
109 /// ```
110 async fn find_or_fail<K>(id: K) -> Result<Self::Model, FrameworkError>
111 where
112 K: Into<<Self::PrimaryKey as PrimaryKeyTrait>::ValueType> + Send + std::fmt::Debug + Copy,
113 {
114 Self::find_by_pk(id).await?.ok_or_else(|| {
115 FrameworkError::database(format!(
116 "{} with id {:?} not found",
117 std::any::type_name::<Self>(),
118 id
119 ))
120 })
121 }
122
123 /// Count all records
124 ///
125 /// # Example
126 /// ```rust,ignore
127 /// let count = user::Entity::count_all().await?;
128 /// ```
129 async fn count_all() -> Result<u64, FrameworkError> {
130 let db = DB::connection()?;
131 Self::find()
132 .count(db.inner())
133 .await
134 .map_err(|e| FrameworkError::database(e.to_string()))
135 }
136
137 /// Check if any records exist
138 ///
139 /// # Example
140 /// ```rust,ignore
141 /// if user::Entity::exists_any().await? {
142 /// println!("Users exist!");
143 /// }
144 /// ```
145 async fn exists_any() -> Result<bool, FrameworkError> {
146 Ok(Self::count_all().await? > 0)
147 }
148
149 /// Get the first record
150 ///
151 /// # Example
152 /// ```rust,ignore
153 /// let first_user = user::Entity::first().await?;
154 /// ```
155 async fn first() -> Result<Option<Self::Model>, FrameworkError> {
156 let db = DB::connection()?;
157 Self::find()
158 .one(db.inner())
159 .await
160 .map_err(|e| FrameworkError::database(e.to_string()))
161 }
162}
163
164/// Trait providing Laravel-like write operations on SeaORM entities
165///
166/// Implement this trait alongside `Model` to get insert/update/delete methods.
167///
168/// # Example
169///
170/// ```rust,ignore
171/// use ferro_rs::database::{Model, ModelMut};
172/// use sea_orm::Set;
173///
174/// // Implement both traits
175/// impl ferro_rs::database::Model for Entity {}
176/// impl ferro_rs::database::ModelMut for Entity {}
177///
178/// // Insert a new record
179/// let new_user = user::ActiveModel {
180/// name: Set("John".to_string()),
181/// email: Set("john@example.com".to_string()),
182/// ..Default::default()
183/// };
184/// let user = user::Entity::insert_one(new_user).await?;
185///
186/// // Delete by ID
187/// user::Entity::delete_by_pk(user.id).await?;
188/// ```
189#[async_trait]
190pub trait ModelMut: Model
191where
192 Self::Model: ModelTrait<Entity = Self> + IntoActiveModel<Self::ActiveModel> + Send + Sync,
193 Self::ActiveModel: ActiveModelTrait<Entity = Self> + ActiveModelBehavior + Send,
194{
195 /// Insert a new record
196 ///
197 /// # Example
198 /// ```rust,ignore
199 /// let new_user = user::ActiveModel {
200 /// name: Set("John".to_string()),
201 /// email: Set("john@example.com".to_string()),
202 /// ..Default::default()
203 /// };
204 /// let user = user::Entity::insert_one(new_user).await?;
205 /// ```
206 async fn insert_one(model: Self::ActiveModel) -> Result<Self::Model, FrameworkError> {
207 let db = DB::connection()?;
208 model
209 .insert(db.inner())
210 .await
211 .map_err(|e| FrameworkError::database(e.to_string()))
212 }
213
214 /// Update an existing record
215 ///
216 /// # Example
217 /// ```rust,ignore
218 /// let mut user: user::ActiveModel = user.into();
219 /// user.name = Set("Updated Name".to_string());
220 /// let updated = user::Entity::update_one(user).await?;
221 /// ```
222 async fn update_one(model: Self::ActiveModel) -> Result<Self::Model, FrameworkError> {
223 let db = DB::connection()?;
224 model
225 .update(db.inner())
226 .await
227 .map_err(|e| FrameworkError::database(e.to_string()))
228 }
229
230 /// Delete a record by primary key
231 ///
232 /// # Example
233 /// ```rust,ignore
234 /// let rows_deleted = user::Entity::delete_by_pk(1).await?;
235 /// ```
236 async fn delete_by_pk<K>(id: K) -> Result<u64, FrameworkError>
237 where
238 K: Into<<Self::PrimaryKey as PrimaryKeyTrait>::ValueType> + Send,
239 {
240 let db = DB::connection()?;
241 let result = Self::delete_by_id(id)
242 .exec(db.inner())
243 .await
244 .map_err(|e| FrameworkError::database(e.to_string()))?;
245 Ok(result.rows_affected)
246 }
247
248 /// Save a model (insert or update based on whether primary key is set)
249 ///
250 /// # Example
251 /// ```rust,ignore
252 /// let user = user::ActiveModel {
253 /// name: Set("John".to_string()),
254 /// ..Default::default()
255 /// };
256 /// let saved = user::Entity::save_one(user).await?;
257 /// ```
258 async fn save_one(model: Self::ActiveModel) -> Result<Self::Model, FrameworkError>
259 where
260 Self::ActiveModel: TryIntoModel<Self::Model>,
261 {
262 let db = DB::connection()?;
263 let saved = model
264 .save(db.inner())
265 .await
266 .map_err(|e| FrameworkError::database(e.to_string()))?;
267 saved
268 .try_into_model()
269 .map_err(|e| FrameworkError::database(e.to_string()))
270 }
271}
272
273// ============================================================================
274// SCOPED QUERIES
275// ============================================================================
276
277/// Trait for defining reusable query scopes on entities
278///
279/// Implement this trait to define common filters that can be applied
280/// to queries in a chainable, reusable way.
281///
282/// # Example
283///
284/// ```rust,ignore
285/// use ferro_rs::database::{ScopedQuery, Scope};
286///
287/// // Define scopes for your entity
288/// pub enum AnimalScope {
289/// ForShelter(i64),
290/// Available,
291/// Species(String),
292/// Size(String),
293/// }
294///
295/// impl Scope<animals::Entity> for AnimalScope {
296/// fn apply(self, query: QueryBuilder<animals::Entity>) -> QueryBuilder<animals::Entity> {
297/// use animals::Column;
298/// match self {
299/// Self::ForShelter(id) => query.filter(Column::ShelterId.eq(id)),
300/// Self::Available => query.filter(Column::Status.eq("available")),
301/// Self::Species(s) => query.filter(Column::Species.eq(s)),
302/// Self::Size(s) => query.filter(Column::Size.eq(s)),
303/// }
304/// }
305/// }
306///
307/// // Implement ScopedQuery for your entity
308/// impl ScopedQuery for animals::Entity {
309/// type Scope = AnimalScope;
310/// }
311///
312/// // Now use scopes in queries:
313/// let dogs = Animal::scoped(AnimalScope::Species("dog".into()))
314/// .and(AnimalScope::Available)
315/// .all()
316/// .await?;
317/// ```
318pub trait ScopedQuery: EntityTrait + Sized
319where
320 Self::Model: Send + Sync,
321{
322 /// The scope type for this entity
323 type Scope: Scope<Self>;
324
325 /// Start a query with the given scope applied
326 fn scoped(scope: Self::Scope) -> ScopedQueryBuilder<Self> {
327 let builder = QueryBuilder::new();
328 ScopedQueryBuilder {
329 inner: scope.apply(builder),
330 }
331 }
332
333 /// Start a query for records belonging to a specific owner
334 ///
335 /// This is a convenience method for common "for_user" or "for_owner" patterns.
336 ///
337 /// # Example
338 ///
339 /// ```rust,ignore
340 /// // If your entity has a user_id column
341 /// let user_favorites = Favorite::for_owner(user.id, Column::UserId).all().await?;
342 /// ```
343 fn for_owner<C, V>(owner_id: V, column: C) -> QueryBuilder<Self>
344 where
345 C: ColumnTrait,
346 V: Into<sea_orm::Value>,
347 {
348 QueryBuilder::new().filter(column.eq(owner_id))
349 }
350}
351
352/// A scope that can be applied to a query
353pub trait Scope<E: EntityTrait>
354where
355 E::Model: Send + Sync,
356{
357 /// Apply this scope to a query builder
358 fn apply(self, query: QueryBuilder<E>) -> QueryBuilder<E>;
359}
360
361/// Query builder with scopes applied
362pub struct ScopedQueryBuilder<E>
363where
364 E: EntityTrait,
365 E::Model: Send + Sync,
366{
367 inner: QueryBuilder<E>,
368}
369
370impl<E> ScopedQueryBuilder<E>
371where
372 E: EntityTrait,
373 E::Model: Send + Sync,
374{
375 /// Add another scope to the query
376 pub fn and<S: Scope<E>>(self, scope: S) -> Self {
377 Self {
378 inner: scope.apply(self.inner),
379 }
380 }
381
382 /// Add a filter condition
383 pub fn filter<F>(self, filter: F) -> Self
384 where
385 F: sea_orm::sea_query::IntoCondition,
386 {
387 Self {
388 inner: self.inner.filter(filter),
389 }
390 }
391
392 /// Get the underlying query builder
393 pub fn into_query(self) -> QueryBuilder<E> {
394 self.inner
395 }
396
397 /// Execute query and return all results
398 pub async fn all(self) -> Result<Vec<E::Model>, FrameworkError> {
399 self.inner.all().await
400 }
401
402 /// Execute query and return first result
403 pub async fn first(self) -> Result<Option<E::Model>, FrameworkError> {
404 self.inner.first().await
405 }
406
407 /// Execute query and return first result or error
408 pub async fn first_or_fail(self) -> Result<E::Model, FrameworkError> {
409 self.inner.first_or_fail().await
410 }
411
412 /// Count matching records
413 pub async fn count(self) -> Result<u64, FrameworkError> {
414 self.inner.count().await
415 }
416
417 /// Check if any records exist
418 pub async fn exists(self) -> Result<bool, FrameworkError> {
419 self.inner.exists().await
420 }
421}
422
423/// Macro to define scopes for an entity
424///
425/// # Example
426///
427/// ```rust,ignore
428/// define_scopes!(animals::Entity {
429/// ForShelter(shelter_id: i64) => Column::ShelterId.eq(shelter_id),
430/// Available => Column::Status.eq("available"),
431/// Species(species: String) => Column::Species.eq(species),
432/// });
433///
434/// // Usage:
435/// let dogs = Animal::scoped(AnimalScope::Species("dog".into())).all().await?;
436/// ```
437#[macro_export]
438macro_rules! define_scopes {
439 ($entity:ty { $($scope_name:ident $(($($arg:ident : $arg_ty:ty),*))? => $filter:expr),* $(,)? }) => {
440 pub enum Scope {
441 $($scope_name $(($($arg_ty),*))?,)*
442 }
443
444 impl $crate::database::Scope<$entity> for Scope {
445 fn apply(self, query: $crate::database::QueryBuilder<$entity>) -> $crate::database::QueryBuilder<$entity> {
446 match self {
447 $(Self::$scope_name $(($($arg),*))? => query.filter($filter),)*
448 }
449 }
450 }
451
452 impl $crate::database::ScopedQuery for $entity {
453 type Scope = Scope;
454 }
455 };
456}
457
458pub use define_scopes;