Skip to main content

shaperail_runtime/db/
store.rs

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