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
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/// Typed resource store implemented by generated per-resource query modules or OrmBackedStore (M14).
16#[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
46/// ORM-backed store (M14). Delegates to OrmResourceQuery using a per-resource connection.
47pub 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
107/// Builds a store registry from DatabaseManager and resources (M14 ORM path).
108/// Each resource gets an OrmBackedStore using the connection for its `db` (or default).
109pub 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")]
130/// Builds a store registry supporting both SQL and MongoDB backends (M14 full multi-DB).
131///
132/// Resources are routed to the appropriate backend based on their `db` field and the
133/// engine type of the corresponding named database config.
134pub 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            // Ensure collection with schema validation.
157            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}