Skip to main content

adminx_seaorm/
lib.rs

1// adminx-seaorm/src/lib.rs
2//
3// SeaORM storage backend for adminx-core. Implements `Storage` against a single
4// `DatabaseConnection` that transparently backs PostgreSQL or MySQL (selected
5// by the connection URL scheme).
6
7mod query;
8
9use adminx_core::storage::{
10    set_storage, CreateOutcome, ListPage, QueryOptions, Storage, StorageError,
11};
12use async_trait::async_trait;
13use sea_orm::sea_query::{Alias, Expr, Query};
14use sea_orm::{
15    ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, ExecResult,
16    FromQueryResult, JsonValue, StatementBuilder,
17};
18use serde_json::{Map, Value};
19
20use query::{
21    build_count_select, build_find_select, build_get_select, build_list_select, id_to_sea_value,
22    json_to_sea_value, value_expr,
23};
24
25/// SeaORM-backed storage. Clone-cheap (the pool is `Arc` internally).
26pub struct SeaOrmStorage {
27    conn: DatabaseConnection,
28}
29
30impl SeaOrmStorage {
31    pub fn new(conn: DatabaseConnection) -> Self {
32        Self { conn }
33    }
34
35    fn backend(&self) -> DbBackend {
36        self.conn.get_database_backend()
37    }
38
39    /// Run a raw SQL statement (schema DDL, seeding). Handy for demos/tests that
40    /// need to create tables before serving.
41    pub async fn execute_sql(&self, sql: &str) -> Result<(), sea_orm::DbErr> {
42        let backend = self.backend();
43        self.conn
44            .execute(sea_orm::Statement::from_string(backend, sql.to_owned()))
45            .await?;
46        Ok(())
47    }
48}
49
50/// Open a connection pool from a database URL and return a ready storage.
51/// e.g. `postgres://user:pass@host/db` or `mysql://user:pass@host/db`.
52pub async fn connect(database_url: &str) -> Result<SeaOrmStorage, sea_orm::DbErr> {
53    let mut opts = ConnectOptions::new(database_url.to_owned());
54    opts.sqlx_logging(false);
55    let conn = Database::connect(opts).await?;
56    tracing::info!(
57        "✅ adminx-seaorm connected ({:?})",
58        conn.get_database_backend()
59    );
60    Ok(SeaOrmStorage::new(conn))
61}
62
63/// Convenience: connect and register as the global adminx storage backend.
64pub async fn init(database_url: &str) -> Result<(), sea_orm::DbErr> {
65    let storage = connect(database_url).await?;
66    set_storage(Box::new(storage));
67    Ok(())
68}
69
70/// Connect and run a batch of **SQL** statements (seeding / migrations), in
71/// order, returning the total rows affected. Self-contained — no `set_storage`
72/// needed. Used by `adminx seed` and by app startup code.
73pub async fn seed(database_url: &str, statements: &[&str]) -> Result<u64, StorageError> {
74    let store = connect(database_url)
75        .await
76        .map_err(|e| StorageError::Backend(e.to_string()))?;
77    let mut total = 0u64;
78    for stmt in statements {
79        total += store.execute_raw(stmt).await?;
80    }
81    Ok(total)
82}
83
84fn last_insert_id(backend: DbBackend, res: &ExecResult) -> Option<String> {
85    match backend {
86        DbBackend::MySql | DbBackend::Sqlite => Some(res.last_insert_id().to_string()),
87        _ => None,
88    }
89}
90
91fn map_err(e: sea_orm::DbErr) -> StorageError {
92    StorageError::Backend(e.to_string())
93}
94
95#[async_trait]
96impl Storage for SeaOrmStorage {
97    async fn list(&self, table: &str, opts: &QueryOptions) -> Result<ListPage, StorageError> {
98        let backend = self.backend();
99
100        // Read the aggregate as a scalar. `JsonValue::find_by_statement` drops
101        // unmapped aggregate columns (returns `{}`) on some backends, so go
102        // through the raw `QueryResult` and pull the `count` column as an i64.
103        let count_stmt =
104            StatementBuilder::build(&build_count_select(table, &opts.filters), &backend);
105        let total: u64 = match self.conn.query_one(count_stmt).await.map_err(map_err)? {
106            Some(qr) => qr.try_get::<i64>("", "count").map_err(map_err)?.max(0) as u64,
107            None => 0,
108        };
109
110        let select = build_list_select(
111            table,
112            opts.per_page,
113            opts.offset(),
114            &opts.sort_by,
115            opts.sort_desc,
116            &opts.filters,
117        );
118        let stmt = StatementBuilder::build(&select, &backend);
119        let rows = JsonValue::find_by_statement(stmt)
120            .all(&self.conn)
121            .await
122            .map_err(map_err)?;
123
124        Ok(ListPage { rows, total })
125    }
126
127    async fn get(&self, table: &str, pk: &str, id: &str) -> Result<Option<Value>, StorageError> {
128        let backend = self.backend();
129        let stmt = StatementBuilder::build(&build_get_select(table, pk, id), &backend);
130        JsonValue::find_by_statement(stmt)
131            .one(&self.conn)
132            .await
133            .map_err(map_err)
134    }
135
136    async fn find_one_by(
137        &self,
138        table: &str,
139        column: &str,
140        value: &str,
141    ) -> Result<Option<Value>, StorageError> {
142        let backend = self.backend();
143        let stmt = StatementBuilder::build(&build_find_select(table, column, value), &backend);
144        JsonValue::find_by_statement(stmt)
145            .one(&self.conn)
146            .await
147            .map_err(map_err)
148    }
149
150    async fn create(
151        &self,
152        table: &str,
153        data: Map<String, Value>,
154    ) -> Result<CreateOutcome, StorageError> {
155        let mut insert = Query::insert();
156        insert.into_table(Alias::new(table));
157        insert.columns(data.keys().map(Alias::new));
158        let values = data.values().map(|v| value_expr(json_to_sea_value(v)));
159        insert
160            .values(values)
161            .map_err(|e| StorageError::Backend(e.to_string()))?;
162
163        let backend = self.backend();
164        let stmt = StatementBuilder::build(&insert, &backend);
165        let res = self.conn.execute(stmt).await.map_err(map_err)?;
166
167        Ok(CreateOutcome {
168            last_insert_id: last_insert_id(backend, &res),
169        })
170    }
171
172    async fn update(
173        &self,
174        table: &str,
175        pk: &str,
176        id: &str,
177        data: Map<String, Value>,
178    ) -> Result<u64, StorageError> {
179        let mut update = Query::update();
180        update.table(Alias::new(table));
181        for (col, val) in &data {
182            update.value(Alias::new(col), json_to_sea_value(val));
183        }
184        update.and_where(Expr::col(Alias::new(pk)).eq(id_to_sea_value(id)));
185
186        let backend = self.backend();
187        let stmt = StatementBuilder::build(&update, &backend);
188        let res = self.conn.execute(stmt).await.map_err(map_err)?;
189        Ok(res.rows_affected())
190    }
191
192    async fn delete(
193        &self,
194        table: &str,
195        pk: &str,
196        id: &str,
197        soft: bool,
198    ) -> Result<u64, StorageError> {
199        let backend = self.backend();
200        let stmt = if soft {
201            let mut update = Query::update();
202            update
203                .table(Alias::new(table))
204                .value(Alias::new("deleted"), true)
205                .and_where(Expr::col(Alias::new(pk)).eq(id_to_sea_value(id)));
206            StatementBuilder::build(&update, &backend)
207        } else {
208            let mut delete = Query::delete();
209            delete
210                .from_table(Alias::new(table))
211                .and_where(Expr::col(Alias::new(pk)).eq(id_to_sea_value(id)));
212            StatementBuilder::build(&delete, &backend)
213        };
214
215        let res = self.conn.execute(stmt).await.map_err(map_err)?;
216        Ok(res.rows_affected())
217    }
218
219    async fn execute_raw(&self, statement: &str) -> Result<u64, StorageError> {
220        let backend = self.backend();
221        let res = self
222            .conn
223            .execute(sea_orm::Statement::from_string(backend, statement.to_owned()))
224            .await
225            .map_err(map_err)?;
226        Ok(res.rows_affected())
227    }
228
229    async fn health(&self) -> bool {
230        self.conn.ping().await.is_ok()
231    }
232}