Skip to main content

cast_core/
model.rs

1//! The `Model` trait — every Cast model implements this.
2
3use std::marker::PhantomData;
4
5use sqlx::FromRow;
6
7use crate::query::QueryBuilder;
8use crate::Error;
9
10/// Inventory record for `#[derive(Model)]`. Lets boost/MCP and other
11/// introspection tools list every model registered at compile time.
12pub struct ModelRegistration {
13    pub class: &'static str,
14    pub table: &'static str,
15    pub columns: &'static [&'static str],
16}
17
18inventory::collect!(ModelRegistration);
19
20pub fn registered_models() -> Vec<&'static ModelRegistration> {
21    inventory::iter::<ModelRegistration>.into_iter().collect()
22}
23
24pub trait Model: Sized + Send + Sync + Unpin + 'static
25where
26    for<'r> Self: FromRow<'r, sqlx::postgres::PgRow>,
27{
28    /// When true, the query builder automatically applies a `deleted_at IS NULL`
29    /// filter to `Model::query()`. Set via `#[soft_deletes]` on the struct.
30    const SOFT_DELETES: bool = false;
31
32    type PrimaryKey: sqlx::Type<sqlx::Postgres>
33        + for<'q> sqlx::Encode<'q, sqlx::Postgres>
34        + Send
35        + Sync
36        + Clone
37        + 'static;
38
39    /// Table name (e.g. `"users"`).
40    const TABLE: &'static str;
41
42    /// Primary key column name (e.g. `"id"`).
43    const PK_COLUMN: &'static str = "id";
44
45    /// All column names in this model's table.
46    const COLUMNS: &'static [&'static str];
47
48    /// Return this row's primary key.
49    fn primary_key(&self) -> &Self::PrimaryKey;
50
51    /// Start a new query builder for this model.
52    fn query() -> QueryBuilder<Self> {
53        QueryBuilder::new()
54    }
55
56    /// Fetch by primary key. Takes a Postgres pool directly — Cast Models are
57    /// Postgres-only in v0.1. For multi-driver schema + raw sqlx, use the
58    /// `cast::Pool` enum.
59    fn find(
60        pool: &sqlx::PgPool,
61        id: Self::PrimaryKey,
62    ) -> futures::future::BoxFuture<'_, Result<Option<Self>, Error>> {
63        Box::pin(async move {
64            let sql = format!(
65                "SELECT {} FROM {} WHERE {} = $1 LIMIT 1",
66                Self::COLUMNS.join(", "),
67                Self::TABLE,
68                Self::PK_COLUMN,
69            );
70            let result = sqlx::query_as::<_, Self>(&sql)
71                .bind(id)
72                .fetch_optional(pool)
73                .await?;
74            Ok(result)
75        })
76    }
77
78    /// Fetch all rows.
79    fn all(pool: &sqlx::PgPool) -> futures::future::BoxFuture<'_, Result<Vec<Self>, Error>> {
80        Box::pin(async move {
81            let sql = format!("SELECT {} FROM {}", Self::COLUMNS.join(", "), Self::TABLE);
82            let rows = sqlx::query_as::<_, Self>(&sql).fetch_all(pool).await?;
83            Ok(rows)
84        })
85    }
86}
87
88/// Wrapper for eager-loaded query results. Generic over the loading shape;
89/// in practice a concrete `LoadedUserWithPosts` type is generated per derive.
90pub struct Loaded<M: Model, R = ()> {
91    pub items: Vec<M>,
92    pub _relations: PhantomData<R>,
93}
94
95impl<M: Model, R> Loaded<M, R> {
96    pub fn new(items: Vec<M>) -> Self {
97        Self {
98            items,
99            _relations: PhantomData,
100        }
101    }
102
103    pub fn into_inner(self) -> Vec<M> {
104        self.items
105    }
106
107    pub fn iter(&self) -> std::slice::Iter<'_, M> {
108        self.items.iter()
109    }
110}