Skip to main content

backbone_bucket/
lib.rs

1//! Bucket Module
2//!
3//! Generated by metaphor-schema. Enhanced with runtime implementations.
4//!
5//! This module provides:
6//! - Domain entities and repositories
7//! - Application services
8//! - HTTP and gRPC handlers
9//! - Route configuration
10//! - State machine enforcement
11//! - Validation rules runtime
12//! - RBAC middleware
13//! - Trigger execution system
14//! - Computed fields
15//! - Workflow orchestrator
16
17#![recursion_limit = "1024"]
18#![allow(unused_imports)]
19
20// Generated modules
21pub mod domain;
22pub mod infrastructure;
23pub mod application;
24pub mod presentation;
25pub mod seeders;
26pub mod exports;
27// <<< CUSTOM MODULES
28pub mod error;
29pub mod storage;
30pub mod auth;
31pub mod config;
32
33// ─── Essential (every consumer wires these) ─────────────────────────────
34pub use error::{BucketError, BucketResult};
35pub use config::{BucketConfig, ConfigEnvError, S3Config, ServingConfig, ServingMode, StorageConfig};
36
37// ─── Storage backends ───────────────────────────────────────────────────
38// `ObjectStorage` is the boundary trait. `LocalStorage` ships always;
39// `S3Storage` is gated on the `s3` feature; `InMemoryStorage` on `test-utils`.
40pub use storage::{ObjectMeta, ObjectStorage, LocalStorage};
41#[cfg(feature = "s3")]
42pub use storage::S3Storage;
43#[cfg(feature = "test-utils")]
44pub use storage::InMemoryStorage;
45
46// ─── Auth slots (consumer-implemented) ──────────────────────────────────
47pub use auth::{
48    ArcAuthzPolicy, AuthExtractor, AuthzDecision, AuthzPolicy, DefaultOwnerOnlyPolicy, HasOwnerId,
49};
50
51// ─── File operations (single-shot programmatic API) ─────────────────────
52pub use application::service::{FileMeta, FileService};
53
54// ─── HTTP surfaces (serving + multipart upload) ─────────────────────────
55pub use presentation::http::{lookup_by_key as lookup_file_by_key, serving_router, ServingContext};
56pub use presentation::http::{
57    upload_router, UploadConfig, UploadContext, DEFAULT_CHUNK_BODY_LIMIT, DEFAULT_UPLOAD_BODY_LIMIT,
58};
59// END CUSTOM
60
61// Re-exports for convenience - Domain entities
62pub use domain::entity::*;
63
64// Re-exports - State Machine
65pub use domain::state_machine::*;
66
67// Re-exports - Computed Fields
68pub use domain::computed::{HasComputedFields, ComputedFieldValues};
69
70// Re-exports - Permissions
71pub use domain::permission::{Action, FieldRestriction, PermissionChecker, PermissionError, PermissionResult};
72
73// Re-exports - Infrastructure
74pub use infrastructure::persistence::*;
75
76// Re-exports - Application services
77pub use application::service::AccessLogService;
78pub use application::service::BucketService;
79pub use application::service::ContentHashService;
80pub use application::service::ConversionJobService;
81pub use application::service::FileCommentService;
82pub use application::service::FileLockService;
83pub use application::service::FileShareService;
84pub use application::service::FileVersionService;
85pub use application::service::ProcessingJobService;
86pub use application::service::StoredFileService;
87pub use application::service::ThumbnailService;
88pub use application::service::UploadSessionService;
89pub use application::service::UserQuotaService;
90
91// <<< CUSTOM - Custom service re-exports
92pub use application::service::LockingService;
93pub use application::service::DeduplicationService;
94pub use application::service::MultipartUploadService;
95pub use application::service::ConversionService;
96#[allow(deprecated)]
97pub use application::service::CdnService;
98pub use application::service::VideoThumbnailService;
99pub use application::service::DocumentPreviewService;
100mod bucket_module; // Phase 6: http_routes() — regeneration-safe BucketModule extensions
101pub use bucket_module::RouterOptions;
102// END CUSTOM
103// Re-exports - Validation
104pub use application::validator::{ValidationError, ValidationResult};
105
106// Re-exports - Triggers
107pub use application::triggers::*;
108
109// Re-exports - Workflows
110pub use application::workflows::*;
111
112use std::sync::Arc;
113use axum::Router;
114use sqlx::PgPool;
115
116/// Bucket module configuration
117///
118/// Use the builder pattern to configure and register this module:
119///
120/// ```text
121/// let bucket = BucketModule::builder()
122///     .with_database(pool.clone())
123///     .build()?;
124///
125/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
126/// let router = bucket.all_crud_routes();
127/// ```
128pub struct BucketModule {
129    pub(crate) access_log_service: Arc<AccessLogService>,
130    pub(crate) bucket_service: Arc<BucketService>,
131    pub(crate) content_hash_service: Arc<ContentHashService>,
132    pub(crate) conversion_job_service: Arc<ConversionJobService>,
133    pub(crate) file_comment_service: Arc<FileCommentService>,
134    pub(crate) file_lock_service: Arc<FileLockService>,
135    pub(crate) file_share_service: Arc<FileShareService>,
136    pub(crate) file_version_service: Arc<FileVersionService>,
137    pub(crate) processing_job_service: Arc<ProcessingJobService>,
138    pub(crate) stored_file_service: Arc<StoredFileService>,
139    pub(crate) thumbnail_service: Arc<ThumbnailService>,
140    pub(crate) upload_session_service: Arc<UploadSessionService>,
141    pub(crate) user_quota_service: Arc<UserQuotaService>,
142    // <<< CUSTOM FIELDS
143    // Custom business logic services
144    pub locking_service: Arc<LockingService>,
145    pub deduplication_service: Arc<DeduplicationService>,
146    pub multipart_upload_service: Arc<MultipartUploadService>,
147    pub conversion_service: Arc<ConversionService>,
148    #[allow(deprecated)]
149    pub cdn_service: Arc<CdnService>,
150    pub video_thumbnail_service: Arc<VideoThumbnailService>,
151    pub document_preview_service: Arc<DocumentPreviewService>,
152    /// Storage backend (mode-B serving + uploads). None until `.with_storage()` is called.
153    pub storage: Option<Arc<dyn ObjectStorage>>,
154    /// Bucket-module config (storage + serving). None until `.with_config()` is called.
155    pub bucket_config: Option<Arc<BucketConfig>>,
156    /// High-level file operations service, wired when both `storage` and `bucket_config` are set.
157    pub file_service: Option<Arc<FileService>>,
158    /// Direct handle to the StoredFile repository — used by the mode-B
159    /// serving handler to look up files by `storage_key`.
160    pub stored_file_repository: Arc<infrastructure::persistence::StoredFileRepository>,
161    // END CUSTOM
162}
163
164impl BucketModule {
165    /// Create a new module builder
166    pub fn builder() -> BucketModuleBuilder {
167        BucketModuleBuilder::new()
168    }
169
170    /// Mount ALL generated CRUD endpoints (12 per entity) with NO domain
171    /// validation — the fully **unguarded** surface. A well-formed request can
172    /// create invalid rows or soft-delete a referenced master out from under its
173    /// dependents. Prefer a guarded composition (read + validated writes) for any
174    /// real deployment; use this only in trusted/admin/seeding contexts.
175    pub fn all_crud_routes(&self) -> Router {
176        use presentation::http::{
177            create_bucket_routes,
178            create_content_hash_routes,
179            create_conversion_job_routes,
180            create_file_comment_routes,
181            create_file_lock_routes,
182            create_file_share_routes,
183            create_processing_job_routes,
184            create_stored_file_routes,
185            create_upload_session_routes,
186            create_user_quota_routes,
187        };
188
189        Router::new()
190            .merge(create_bucket_routes(self.bucket_service.clone()))
191            .merge(create_content_hash_routes(self.content_hash_service.clone()))
192            .merge(create_conversion_job_routes(self.conversion_job_service.clone()))
193            .merge(create_file_comment_routes(self.file_comment_service.clone()))
194            .merge(create_file_lock_routes(self.file_lock_service.clone()))
195            .merge(create_file_share_routes(self.file_share_service.clone()))
196            .merge(create_processing_job_routes(self.processing_job_service.clone()))
197            .merge(create_stored_file_routes(self.stored_file_service.clone()))
198            .merge(create_upload_session_routes(self.upload_session_service.clone()))
199            .merge(create_user_quota_routes(self.user_quota_service.clone()))
200    }
201
202    /// Deprecated alias for [`Self::all_crud_routes`]. `routes()` reads like
203    /// "the routes" but mounts UNVALIDATED generic CRUD on every entity — a naive
204    /// mount exposes unguarded writes. Compose a guarded router (read + validated
205    /// writes) for production, or call `all_crud_routes()` to opt into the full
206    /// unguarded surface explicitly.
207    #[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + validated writes, or all_crud_routes() for the full/unguarded surface")]
208    pub fn routes(&self) -> Router {
209        self.all_crud_routes()
210    }
211
212    /// Read-only routes for every entity (GET endpoints only) — the safe base.
213    ///
214    /// Generic mutation can't reach here, so this surface cannot bypass a
215    /// validated write service's invariants. Use this as the production base and
216    /// merge validated write routes (or a write service's HTTP layer) onto it.
217    pub fn readonly_routes(&self) -> Router {
218        use presentation::http::{
219            create_bucket_read_routes,
220            create_content_hash_read_routes,
221            create_conversion_job_read_routes,
222            create_file_comment_read_routes,
223            create_file_lock_read_routes,
224            create_file_share_read_routes,
225            create_processing_job_read_routes,
226            create_stored_file_read_routes,
227            create_upload_session_read_routes,
228            create_user_quota_read_routes,
229        };
230
231        Router::new()
232            .merge(create_bucket_read_routes(self.bucket_service.clone()))
233            .merge(create_content_hash_read_routes(self.content_hash_service.clone()))
234            .merge(create_conversion_job_read_routes(self.conversion_job_service.clone()))
235            .merge(create_file_comment_read_routes(self.file_comment_service.clone()))
236            .merge(create_file_lock_read_routes(self.file_lock_service.clone()))
237            .merge(create_file_share_read_routes(self.file_share_service.clone()))
238            .merge(create_processing_job_read_routes(self.processing_job_service.clone()))
239            .merge(create_stored_file_read_routes(self.stored_file_service.clone()))
240            .merge(create_upload_session_read_routes(self.upload_session_service.clone()))
241            .merge(create_user_quota_read_routes(self.user_quota_service.clone()))
242    }
243
244    // <<< CUSTOM METHODS
245    /// What [`upload_router`] needs, built from this module's own services.
246    ///
247    /// `None` until the module has both a storage backend and a config (the
248    /// file service is wired only then). A host mounts uploads with
249    /// `upload_router::<I>(bucket.upload_context()?, config)` and never holds
250    /// the generic bucket service itself.
251    pub fn upload_context(&self) -> Option<UploadContext> {
252        Some(UploadContext {
253            file_service: self.file_service.clone()?,
254            multipart_service: self.multipart_upload_service.clone(),
255            bucket_service: self.bucket_service.clone(),
256            storage: self.storage.clone()?,
257        })
258    }
259    // END CUSTOM
260}
261
262/// Builder for BucketModule
263pub struct BucketModuleBuilder {
264    db_pool: Option<PgPool>,
265    // <<< CUSTOM BUILDER FIELDS
266    // bucket-serving fields (see docs/serving.md)
267    storage: Option<Arc<dyn ObjectStorage>>,
268    bucket_config: Option<BucketConfig>,
269    // END CUSTOM
270}
271
272impl BucketModuleBuilder {
273    /// Create a new builder
274    pub fn new() -> Self {
275        Self {
276            db_pool: None,
277            // <<< CUSTOM BUILDER DEFAULTS
278            storage: None,
279            bucket_config: None,
280            // END CUSTOM
281        }
282    }
283
284    /// Set the database connection pool
285    pub fn with_database(mut self, pool: PgPool) -> Self {
286        self.db_pool = Some(pool);
287        self
288    }
289
290    // <<< CUSTOM - custom builder methods
291    // Bucket-serving builder methods (see docs/serving.md)
292
293    /// Attach the bucket-module runtime config (storage + serving).
294    pub fn with_config(mut self, config: BucketConfig) -> Self {
295        self.bucket_config = Some(config);
296        self
297    }
298
299    /// Attach the object-storage backend (`LocalStorage`, `S3Storage`, …).
300    pub fn with_storage(mut self, storage: Arc<dyn ObjectStorage>) -> Self {
301        self.storage = Some(storage);
302        self
303    }
304
305    // END CUSTOM
306
307    /// Build the module with configured dependencies
308    pub fn build(self) -> anyhow::Result<BucketModule> {
309        let db_pool = self.db_pool
310            .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;
311
312        // AccessLog service
313        let access_log_repository = Arc::new(AccessLogRepository::new(db_pool.clone()));
314        let access_log_service = Arc::new(AccessLogService::with_repository(access_log_repository.clone()));
315
316        // Bucket service
317        let bucket_repository = Arc::new(BucketRepository::new(db_pool.clone()));
318        let bucket_service = Arc::new(BucketService::with_repository(bucket_repository.clone()));
319
320        // ContentHash service
321        let content_hash_repository = Arc::new(ContentHashRepository::new(db_pool.clone()));
322        let content_hash_service = Arc::new(ContentHashService::with_repository(content_hash_repository.clone()));
323
324        // ConversionJob service
325        let conversion_job_repository = Arc::new(ConversionJobRepository::new(db_pool.clone()));
326        let conversion_job_service = Arc::new(ConversionJobService::with_repository(conversion_job_repository.clone()));
327
328        // FileComment service
329        let file_comment_repository = Arc::new(FileCommentRepository::new(db_pool.clone()));
330        let file_comment_service = Arc::new(FileCommentService::with_repository(file_comment_repository.clone()));
331
332        // FileLock service
333        let file_lock_repository = Arc::new(FileLockRepository::new(db_pool.clone()));
334        let file_lock_service = Arc::new(FileLockService::with_repository(file_lock_repository.clone()));
335
336        // FileShare service
337        let file_share_repository = Arc::new(FileShareRepository::new(db_pool.clone()));
338        let file_share_service = Arc::new(FileShareService::with_repository(file_share_repository.clone()));
339
340        // FileVersion service
341        let file_version_repository = Arc::new(FileVersionRepository::new(db_pool.clone()));
342        let file_version_service = Arc::new(FileVersionService::with_repository(file_version_repository.clone()));
343
344        // ProcessingJob service
345        let processing_job_repository = Arc::new(ProcessingJobRepository::new(db_pool.clone()));
346        let processing_job_service = Arc::new(ProcessingJobService::with_repository(processing_job_repository.clone()));
347
348        // StoredFile service
349        let stored_file_repository = Arc::new(StoredFileRepository::new(db_pool.clone()));
350        let stored_file_service = Arc::new(StoredFileService::with_repository(stored_file_repository.clone()));
351
352        // Thumbnail service
353        let thumbnail_repository = Arc::new(ThumbnailRepository::new(db_pool.clone()));
354        let thumbnail_service = Arc::new(ThumbnailService::with_repository(thumbnail_repository.clone()));
355
356        // UploadSession service
357        let upload_session_repository = Arc::new(UploadSessionRepository::new(db_pool.clone()));
358        let upload_session_service = Arc::new(UploadSessionService::with_repository(upload_session_repository.clone()));
359
360        // UserQuota service
361        let user_quota_repository = Arc::new(UserQuotaRepository::new(db_pool.clone()));
362        let user_quota_service = Arc::new(UserQuotaService::with_repository(user_quota_repository.clone()));
363
364        // <<< CUSTOM
365        // Custom business logic services
366        let locking_service = Arc::new(LockingService::new(
367            file_lock_repository, stored_file_repository.clone(),
368        ));
369        let deduplication_service = Arc::new(DeduplicationService::new(
370            content_hash_repository, stored_file_repository.clone(),
371        ));
372        let multipart_upload_service = Arc::new(MultipartUploadService::new(
373            upload_session_repository, bucket_repository.clone(), user_quota_repository,
374        ));
375        let conversion_service = Arc::new(ConversionService::new(
376            conversion_job_repository, stored_file_repository.clone(),
377        ));
378        #[allow(deprecated)]
379        let cdn_service = Arc::new(CdnService::new(
380            stored_file_repository.clone(), bucket_repository,
381        ));
382        let video_thumbnail_service = Arc::new(VideoThumbnailService::new(
383            processing_job_repository.clone(), thumbnail_repository.clone(), stored_file_repository.clone(),
384        ));
385        let document_preview_service = Arc::new(DocumentPreviewService::new(
386            processing_job_repository, thumbnail_repository, stored_file_repository.clone(),
387        ));
388
389        // Bucket-serving wiring (see docs/serving.md)
390        let bucket_config = self.bucket_config.map(Arc::new);
391        let storage = self.storage;
392        let file_service = match (&storage, &bucket_config) {
393            (Some(s), Some(c)) => Some(Arc::new(FileService::new(
394                s.clone(),
395                stored_file_repository.clone(),
396                c.clone(),
397            ))),
398            _ => None,
399        };
400        // END CUSTOM
401
402        Ok(BucketModule {
403            access_log_service,
404            bucket_service,
405            content_hash_service,
406            conversion_job_service,
407            file_comment_service,
408            file_lock_service,
409            file_share_service,
410            file_version_service,
411            processing_job_service,
412            stored_file_service,
413            thumbnail_service,
414            upload_session_service,
415            user_quota_service,
416            // <<< CUSTOM
417            locking_service,
418            deduplication_service,
419            multipart_upload_service,
420            conversion_service,
421            cdn_service,
422            video_thumbnail_service,
423            document_preview_service,
424            storage,
425            bucket_config,
426            file_service,
427            stored_file_repository,
428            // END CUSTOM
429        })
430    }
431}
432
433impl Default for BucketModuleBuilder {
434    fn default() -> Self {
435        Self::new()
436    }
437}