1mod convert;
14
15use adminx_core::storage::{
16 set_storage, CreateOutcome, FilterClause, FilterOp, ListPage, QueryOptions, Storage,
17 StorageError,
18};
19use async_trait::async_trait;
20use futures_util::stream::TryStreamExt;
21use mongodb::bson::{doc, to_document, Bson, Document};
22use mongodb::options::FindOptions;
23use mongodb::{Client, Collection, Database};
24use serde_json::{Map, Value};
25
26use convert::{bson_to_json, doc_to_json, id_filter, json_map_to_doc};
27
28pub struct MongoStorage {
30 db: Database,
31}
32
33impl MongoStorage {
34 pub fn new(db: Database) -> Self {
35 Self { db }
36 }
37
38 fn collection(&self, name: &str) -> Collection<Document> {
39 self.db.collection::<Document>(name)
40 }
41}
42
43pub async fn connect(uri: &str, db_name: &str) -> Result<MongoStorage, mongodb::error::Error> {
45 let client = Client::with_uri_str(uri).await?;
46 let db = client.database(db_name);
47 tracing::info!("✅ adminx-mongo connected to database '{}'", db_name);
48 Ok(MongoStorage::new(db))
49}
50
51pub async fn init(uri: &str, db_name: &str) -> Result<(), mongodb::error::Error> {
53 let storage = connect(uri, db_name).await?;
54 set_storage(Box::new(storage));
55 Ok(())
56}
57
58pub async fn seed(uri: &str, db_name: &str, statements: &[&str]) -> Result<u64, StorageError> {
63 let store = connect(uri, db_name).await.map_err(map_err)?;
64 let mut total = 0u64;
65 for stmt in statements {
66 total += store.execute_raw(stmt).await?;
67 }
68 Ok(total)
69}
70
71fn map_err(e: mongodb::error::Error) -> StorageError {
72 StorageError::Backend(e.to_string())
73}
74
75#[async_trait]
76impl Storage for MongoStorage {
77 async fn list(&self, table: &str, opts: &QueryOptions) -> Result<ListPage, StorageError> {
78 let collection = self.collection(table);
79 let query = build_filter_doc(&opts.filters);
80
81 let total = collection
82 .count_documents(query.clone(), None)
83 .await
84 .map_err(map_err)?;
85
86 let sort = opts.sort_by.as_ref().map(|col| {
87 let dir = if opts.sort_desc { -1i32 } else { 1i32 };
88 doc! { col: dir }
89 });
90 let find_options = FindOptions::builder()
91 .skip(Some(opts.offset()))
92 .limit(Some(opts.per_page as i64))
93 .sort(sort)
94 .build();
95
96 let mut cursor = collection
97 .find(query, find_options)
98 .await
99 .map_err(map_err)?;
100
101 let mut rows = Vec::new();
102 while let Some(doc) = cursor.try_next().await.map_err(map_err)? {
103 rows.push(doc_to_json(doc));
104 }
105
106 Ok(ListPage { rows, total })
107 }
108
109 async fn get(&self, table: &str, pk: &str, id: &str) -> Result<Option<Value>, StorageError> {
110 let doc = self
111 .collection(table)
112 .find_one(id_filter(pk, id), None)
113 .await
114 .map_err(map_err)?;
115 Ok(doc.map(doc_to_json))
116 }
117
118 async fn find_one_by(
119 &self,
120 table: &str,
121 column: &str,
122 value: &str,
123 ) -> Result<Option<Value>, StorageError> {
124 let doc = self
125 .collection(table)
126 .find_one(doc! { column: value }, None)
127 .await
128 .map_err(map_err)?;
129 Ok(doc.map(doc_to_json))
130 }
131
132 async fn create(
133 &self,
134 table: &str,
135 data: Map<String, Value>,
136 ) -> Result<CreateOutcome, StorageError> {
137 let document = json_map_to_doc(data);
138 let res = self
139 .collection(table)
140 .insert_one(document, None)
141 .await
142 .map_err(map_err)?;
143
144 let last_insert_id = match bson_to_json(res.inserted_id) {
145 Value::String(s) => Some(s),
146 other => Some(other.to_string()),
147 };
148 Ok(CreateOutcome { last_insert_id })
149 }
150
151 async fn update(
152 &self,
153 table: &str,
154 pk: &str,
155 id: &str,
156 data: Map<String, Value>,
157 ) -> Result<u64, StorageError> {
158 let set = json_map_to_doc(data);
159 let res = self
160 .collection(table)
161 .update_one(id_filter(pk, id), doc! { "$set": set }, None)
162 .await
163 .map_err(map_err)?;
164 Ok(res.modified_count)
165 }
166
167 async fn delete(
168 &self,
169 table: &str,
170 pk: &str,
171 id: &str,
172 soft: bool,
173 ) -> Result<u64, StorageError> {
174 let collection = self.collection(table);
175 let filter = id_filter(pk, id);
176
177 if soft {
178 let res = collection
179 .update_one(filter, doc! { "$set": { "deleted": true } }, None)
180 .await
181 .map_err(map_err)?;
182 Ok(res.modified_count)
183 } else {
184 let res = collection.delete_one(filter, None).await.map_err(map_err)?;
185 Ok(res.deleted_count)
186 }
187 }
188
189 async fn execute_raw(&self, statement: &str) -> Result<u64, StorageError> {
190 let value: Value = serde_json::from_str(statement)
193 .map_err(|e| StorageError::Backend(format!("invalid JSON command: {e}")))?;
194 let command = to_document(&value)
195 .map_err(|e| StorageError::Backend(format!("command is not a document: {e}")))?;
196 let res = self.db.run_command(command, None).await.map_err(map_err)?;
197 let n = res
199 .get_i64("n")
200 .ok()
201 .or_else(|| res.get_i32("n").ok().map(|v| v as i64))
202 .unwrap_or(0);
203 Ok(n.max(0) as u64)
204 }
205
206 async fn health(&self) -> bool {
207 self.db.run_command(doc! { "ping": 1 }, None).await.is_ok()
208 }
209}
210
211fn filter_bson(v: &str) -> Bson {
214 match v {
215 "true" => Bson::Boolean(true),
216 "false" => Bson::Boolean(false),
217 _ => match v.parse::<i64>() {
218 Ok(i) => Bson::Int64(i),
219 Err(_) => Bson::String(v.to_owned()),
220 },
221 }
222}
223
224fn build_filter_doc(filters: &[FilterClause]) -> Document {
228 let mut doc = Document::new();
229 for f in filters {
230 match f.op {
231 FilterOp::Eq => {
232 doc.insert(f.field.clone(), filter_bson(&f.value));
233 }
234 FilterOp::Contains => {
235 doc.insert(
236 f.field.clone(),
237 doc! { "$regex": f.value.clone(), "$options": "i" },
238 );
239 }
240 FilterOp::Gte | FilterOp::Lte => {
241 let key = if f.op == FilterOp::Gte { "$gte" } else { "$lte" };
242 let mut sub = doc
244 .get_document(&f.field)
245 .cloned()
246 .unwrap_or_default();
247 sub.insert(key, filter_bson(&f.value));
248 doc.insert(f.field.clone(), sub);
249 }
250 }
251 }
252 doc
253}