1use std::collections::HashMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use serde_json::{Map, Value};
6
7#[cfg(feature = "multi-db")]
8use super::mongo::{MongoBackedStore, MongoConnection};
9use super::{DatabaseManager, ResourceRow};
10use super::{FilterSet, OrmResourceQuery, PageRequest, SearchParam, SortParam, SqlConnection};
11#[cfg(feature = "multi-db")]
12use shaperail_core::{DatabaseEngine, NamedDatabaseConfig};
13use shaperail_core::{EndpointSpec, ResourceDefinition, ShaperailError};
14
15#[async_trait]
17pub trait ResourceStore: Send + Sync {
18 fn resource_name(&self) -> &str;
19
20 async fn find_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError>;
21
22 async fn find_all(
23 &self,
24 endpoint: &EndpointSpec,
25 filters: &FilterSet,
26 search: Option<&SearchParam>,
27 sort: &SortParam,
28 page: &PageRequest,
29 ) -> Result<(Vec<ResourceRow>, Value), ShaperailError>;
30
31 async fn insert(&self, data: &Map<String, Value>) -> Result<ResourceRow, ShaperailError>;
32
33 async fn update_by_id(
34 &self,
35 id: &uuid::Uuid,
36 data: &Map<String, Value>,
37 ) -> Result<ResourceRow, ShaperailError>;
38
39 async fn soft_delete_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError>;
40
41 async fn hard_delete_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError>;
42}
43
44pub type StoreRegistry = Arc<HashMap<String, Arc<dyn ResourceStore>>>;
45
46pub struct OrmBackedStore {
48 resource: Arc<ResourceDefinition>,
49 connection: SqlConnection,
50}
51
52impl OrmBackedStore {
53 pub fn new(resource: Arc<ResourceDefinition>, connection: SqlConnection) -> Self {
54 Self {
55 resource,
56 connection,
57 }
58 }
59
60 fn orm(&self) -> OrmResourceQuery<'_> {
61 OrmResourceQuery::new(self.resource.as_ref(), &self.connection)
62 }
63}
64
65#[async_trait]
66impl ResourceStore for OrmBackedStore {
67 fn resource_name(&self) -> &str {
68 &self.resource.resource
69 }
70
71 async fn find_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError> {
72 self.orm().find_by_id(id).await
73 }
74
75 async fn find_all(
76 &self,
77 _endpoint: &EndpointSpec,
78 filters: &FilterSet,
79 search: Option<&SearchParam>,
80 sort: &SortParam,
81 page: &PageRequest,
82 ) -> Result<(Vec<ResourceRow>, Value), ShaperailError> {
83 self.orm().find_all(filters, search, sort, page).await
84 }
85
86 async fn insert(&self, data: &Map<String, Value>) -> Result<ResourceRow, ShaperailError> {
87 self.orm().insert(data).await
88 }
89
90 async fn update_by_id(
91 &self,
92 id: &uuid::Uuid,
93 data: &Map<String, Value>,
94 ) -> Result<ResourceRow, ShaperailError> {
95 self.orm().update_by_id(id, data).await
96 }
97
98 async fn soft_delete_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError> {
99 self.orm().soft_delete_by_id(id).await
100 }
101
102 async fn hard_delete_by_id(&self, id: &uuid::Uuid) -> Result<ResourceRow, ShaperailError> {
103 self.orm().hard_delete_by_id(id).await
104 }
105}
106
107pub fn build_orm_store_registry(
110 manager: &DatabaseManager,
111 resources: &[ResourceDefinition],
112) -> Result<StoreRegistry, ShaperailError> {
113 let mut stores: HashMap<String, Arc<dyn ResourceStore>> = HashMap::new();
114 for resource in resources {
115 let conn = manager
116 .sql_for_resource(resource.db.as_ref())
117 .ok_or_else(|| {
118 ShaperailError::Internal(format!(
119 "No SQL connection for resource '{}' (db: {:?})",
120 resource.resource, resource.db
121 ))
122 })?;
123 let store = OrmBackedStore::new(Arc::new(resource.clone()), conn);
124 stores.insert(resource.resource.clone(), Arc::new(store));
125 }
126 Ok(Arc::new(stores))
127}
128
129#[cfg(feature = "multi-db")]
130pub async fn build_multi_store_registry(
135 manager: &DatabaseManager,
136 mongo_connections: &HashMap<String, MongoConnection>,
137 databases: &indexmap::IndexMap<String, NamedDatabaseConfig>,
138 resources: &[ResourceDefinition],
139) -> Result<StoreRegistry, ShaperailError> {
140 let mut stores: HashMap<String, Arc<dyn ResourceStore>> = HashMap::new();
141
142 for resource in resources {
143 let db_name = resource.db.as_deref().unwrap_or("default");
144 let engine = databases
145 .get(db_name)
146 .map(|cfg| cfg.engine)
147 .unwrap_or(DatabaseEngine::Postgres);
148
149 if engine == DatabaseEngine::MongoDB {
150 let mongo = mongo_connections.get(db_name).ok_or_else(|| {
151 ShaperailError::Internal(format!(
152 "No MongoDB connection for resource '{}' (db: '{db_name}')",
153 resource.resource
154 ))
155 })?;
156 mongo.ensure_collection(resource).await?;
158 let store = MongoBackedStore::new(Arc::new(resource.clone()), mongo.clone());
159 stores.insert(resource.resource.clone(), Arc::new(store));
160 } else {
161 let conn = manager
162 .sql_for_resource(resource.db.as_ref())
163 .ok_or_else(|| {
164 ShaperailError::Internal(format!(
165 "No SQL connection for resource '{}' (db: '{db_name}')",
166 resource.resource
167 ))
168 })?;
169 let store = OrmBackedStore::new(Arc::new(resource.clone()), conn);
170 stores.insert(resource.resource.clone(), Arc::new(store));
171 }
172 }
173 Ok(Arc::new(stores))
174}