1#![recursion_limit = "1024"]
18#![allow(unused_imports)]
19
20pub mod domain;
22pub mod infrastructure;
23pub mod application;
24pub mod presentation;
25pub mod seeders;
26
27pub mod handlers; pub mod routes; pub mod exports; use domain::entity::*;
39use infrastructure::persistence::*;
40
41pub use application::service::AttributeService;
43pub use application::service::AttributeValueService;
44pub use application::service::BrandService;
45pub use application::service::ItemService;
46pub use application::service::ItemGroupService;
47pub use application::service::ItemVariantService;
48pub use application::service::UomService;
49pub use application::service::UomConversionService;
50
51pub use application::service::{
53 CatalogWriteError, CatalogWriteService, NewAttribute, NewAttributeValue, NewBrand, NewItem,
54 NewItemGroup, NewItemVariant, NewUom,
55};
56pub use domain::services::uom_tree::{
58 convert_quantity, ConversionRounding, UomChain, UomChainNode, UomConversionError,
59};
60pub use presentation::http::create_guarded_catalog_routes;
61use std::sync::Arc;
63use axum::Router;
64use sqlx::PgPool;
65
66pub async fn assert_rls_enforced(pool: &PgPool) -> anyhow::Result<()> {
75 let is_super: bool = sqlx::query_scalar("SELECT current_setting('is_superuser')::boolean")
76 .fetch_one(pool)
77 .await?;
78 if is_super {
79 anyhow::bail!(
80 "backbone-catalog RLS guard: the database connection is a SUPERUSER, which bypasses \
81 ROW LEVEL SECURITY and defeats the composing host's org fence. Connect the app as \
82 a non-superuser role (migrations/seeders may still run as the owner)."
83 );
84 }
85 Ok(())
86}
87
88pub struct CatalogModule {
108 pub(crate) attribute_service: Arc<AttributeService>,
109 pub(crate) attribute_value_service: Arc<AttributeValueService>,
110 pub(crate) brand_service: Arc<BrandService>,
111 pub(crate) item_service: Arc<ItemService>,
112 pub(crate) item_group_service: Arc<ItemGroupService>,
113 pub(crate) item_variant_service: Arc<ItemVariantService>,
114 pub(crate) uom_service: Arc<UomService>,
115 pub(crate) uom_conversion_service: Arc<UomConversionService>,
116 pub(crate) catalog_write_service: Arc<CatalogWriteService>,
120 }
122
123impl CatalogModule {
124 pub fn builder() -> CatalogModuleBuilder {
126 CatalogModuleBuilder::new()
127 }
128
129 #[cfg(any(test, feature = "unguarded"))]
135 pub fn all_crud_routes(&self) -> Router {
136 use presentation::http::{
137 create_attribute_routes,
138 create_attribute_value_routes,
139 create_brand_routes,
140 create_item_routes,
141 create_item_group_routes,
142 create_item_variant_routes,
143 create_uom_routes,
144 create_uom_conversion_routes,
145 };
146
147 Router::new()
148 .merge(create_attribute_routes(self.attribute_service.clone()))
149 .merge(create_attribute_value_routes(self.attribute_value_service.clone()))
150 .merge(create_brand_routes(self.brand_service.clone()))
151 .merge(create_item_routes(self.item_service.clone()))
152 .merge(create_item_group_routes(self.item_group_service.clone()))
153 .merge(create_item_variant_routes(self.item_variant_service.clone()))
154 .merge(create_uom_routes(self.uom_service.clone()))
155 .merge(create_uom_conversion_routes(self.uom_conversion_service.clone()))
156 }
157
158 #[cfg(any(test, feature = "unguarded"))]
164 #[deprecated(note = "mounts unvalidated generic CRUD on every entity; compose a guarded router for production, or call all_crud_routes() for the intentional full/unguarded surface")]
165 pub fn routes(&self) -> Router {
166 self.all_crud_routes()
167 }
168}
169
170pub struct CatalogModuleBuilder {
172 db_pool: Option<PgPool>,
173}
174
175impl CatalogModuleBuilder {
176 pub fn new() -> Self {
178 Self {
179 db_pool: None,
180 }
181 }
182
183 pub fn with_database(mut self, pool: PgPool) -> Self {
185 self.db_pool = Some(pool);
186 self
187 }
188
189 pub fn build(self) -> anyhow::Result<CatalogModule> {
194 let db_pool = self.db_pool
195 .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;
196
197 let attribute_repository = Arc::new(AttributeRepository::new(db_pool.clone()));
199 let attribute_service = Arc::new(AttributeService::with_repository(attribute_repository.clone()));
200
201 let attribute_value_repository = Arc::new(AttributeValueRepository::new(db_pool.clone()));
203 let attribute_value_service = Arc::new(AttributeValueService::with_repository(attribute_value_repository.clone()));
204
205 let brand_repository = Arc::new(BrandRepository::new(db_pool.clone()));
207 let brand_service = Arc::new(BrandService::with_repository(brand_repository.clone()));
208
209 let item_repository = Arc::new(ItemRepository::new(db_pool.clone()));
211 let item_service = Arc::new(ItemService::with_repository(item_repository.clone()));
212
213 let item_group_repository = Arc::new(ItemGroupRepository::new(db_pool.clone()));
215 let item_group_service = Arc::new(ItemGroupService::with_repository(item_group_repository.clone()));
216
217 let item_variant_repository = Arc::new(ItemVariantRepository::new(db_pool.clone()));
219 let item_variant_service = Arc::new(ItemVariantService::with_repository(item_variant_repository.clone()));
220
221 let uom_repository = Arc::new(UomRepository::new(db_pool.clone()));
223 let uom_service = Arc::new(UomService::with_repository(uom_repository.clone()));
224
225 let uom_conversion_repository = Arc::new(UomConversionRepository::new(db_pool.clone()));
227 let uom_conversion_service = Arc::new(UomConversionService::with_repository(uom_conversion_repository.clone()));
228
229 let catalog_write_service = Arc::new(CatalogWriteService::new(db_pool.clone()));
231 Ok(CatalogModule {
234 attribute_service,
235 attribute_value_service,
236 brand_service,
237 item_service,
238 item_group_service,
239 item_variant_service,
240 uom_service,
241 uom_conversion_service,
242 catalog_write_service,
244 })
246 }
247}
248
249impl Default for CatalogModuleBuilder {
250 fn default() -> Self {
251 Self::new()
252 }
253}