From 6b02c109429860bb63123a6c7fe3dd162272573c Mon Sep 17 00:00:00 2001 From: Diocrafts Date: Sat, 7 Mar 2026 18:05:52 +0100 Subject: [PATCH] =?UTF-8?q?perf:=20complete=20storage-layer=20UUID=20migra?= =?UTF-8?q?tion=20=E2=80=94=20eliminate=20UUID=E2=86=94String=20round-trip?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Domain: Folder.owner_id + File.owner_id → Option (was Option) - Traits: folder_repository signatures accept Uuid directly (was &str) - Infra: remove all fi.user_id::text / fo.user_id::text SQL casts in repos - Infra: remove $N::uuid bind casts (columns are native UUID now) - Services: eliminate to_string() bridge variables in folder_service, search_service, trash_service - DTOs: boundary conversion to String only at API edge (file_dto, folder_dto) - Ports: default impls compare Option directly - Stubs: updated signatures to match trait 13 files changed across domain/application/infrastructure layers. Compiles clean (cargo check passes). --- src/application/dtos/file_dto.rs | 2 +- src/application/dtos/folder_dto.rs | 2 +- src/application/ports/storage_ports.rs | 6 +- src/application/services/folder_service.rs | 29 +++-- src/application/services/search_service.rs | 4 +- src/application/services/trash_service.rs | 2 +- src/common/stubs.rs | 6 +- src/domain/entities/file.rs | 12 +- src/domain/entities/folder.rs | 12 +- src/domain/repositories/folder_repository.rs | 11 +- .../pg/file_blob_read_repository.rs | 58 +++++----- .../pg/file_blob_write_repository.rs | 13 ++- .../repositories/pg/folder_db_repository.rs | 103 +++++++++--------- 13 files changed, 131 insertions(+), 129 deletions(-) diff --git a/src/application/dtos/file_dto.rs b/src/application/dtos/file_dto.rs index af3c0553..0fcd289a 100755 --- a/src/application/dtos/file_dto.rs +++ b/src/application/dtos/file_dto.rs @@ -83,7 +83,7 @@ impl From for FileDto { icon_special_class, category, size_formatted, - owner_id: parts.owner_id, + owner_id: parts.owner_id.map(|u| u.to_string()), sort_date: None, } } diff --git a/src/application/dtos/folder_dto.rs b/src/application/dtos/folder_dto.rs index 32e7ce4e..0b1f64f4 100755 --- a/src/application/dtos/folder_dto.rs +++ b/src/application/dtos/folder_dto.rs @@ -75,7 +75,7 @@ impl From for FolderDto { name: folder.name().to_string(), path: folder.path_string().to_string(), parent_id: folder.parent_id().map(String::from), - owner_id: folder.owner_id().map(String::from), + owner_id: folder.owner_id().map(|u| u.to_string()), created_at: folder.created_at(), modified_at: folder.modified_at(), is_root, diff --git a/src/application/ports/storage_ports.rs b/src/application/ports/storage_ports.rs index 7fed9628..d4bef15d 100755 --- a/src/application/ports/storage_ports.rs +++ b/src/application/ports/storage_ports.rs @@ -57,10 +57,9 @@ pub trait FileReadPort: Send + Sync + 'static { owner_id: Uuid, ) -> Result, DomainError> { let all = self.list_files(folder_id).await?; - let owner_str = owner_id.to_string(); Ok(all .into_iter() - .filter(|f| f.owner_id().is_some_and(|o| o == owner_str)) + .filter(|f| f.owner_id() == Some(owner_id)) .collect()) } @@ -142,10 +141,9 @@ pub trait FileReadPort: Send + Sync + 'static { ) -> Result, DomainError> { // Default: filter in-memory (repos should override with SQL) let all = self.list_files_batch(folder_id, offset, limit).await?; - let owner_str = owner_id.to_string(); Ok(all .into_iter() - .filter(|f| f.owner_id().is_some_and(|o| o == owner_str)) + .filter(|f| f.owner_id() == Some(owner_id)) .collect()) } diff --git a/src/application/services/folder_service.rs b/src/application/services/folder_service.rs index c665ded6..f6fbf1e9 100755 --- a/src/application/services/folder_service.rs +++ b/src/application/services/folder_service.rs @@ -174,10 +174,9 @@ impl FolderUseCase for FolderService { user_id: Uuid, name: String, ) -> Result { - let user_id_str = user_id.to_string(); let folder = self .folder_storage - .create_home_folder(&user_id_str, name) + .create_home_folder(user_id, name) .await .map_err(|e| { DomainError::internal_error( @@ -208,9 +207,8 @@ impl FolderUseCase for FolderService { /// Gets a folder by its ID, enforcing that `caller_id` is the owner. async fn get_folder_owned(&self, id: &str, caller_id: Uuid) -> Result { - let caller_id_str = caller_id.to_string(); let folder_dto = self.get_folder(id).await?; - if folder_dto.owner_id.as_deref() != Some(caller_id_str.as_str()) { + if folder_dto.owner_id.as_deref() != Some(&caller_id.to_string()) { tracing::warn!( "get_folder_owned: user '{}' attempted to access folder '{}' owned by '{:?}'", caller_id, @@ -265,10 +263,13 @@ impl FolderUseCase for FolderService { parent_id: Option<&str>, owner_id: Uuid, ) -> Result, DomainError> { - let owner_id_str = owner_id.to_string(); + let owner_id_short = { + let s = owner_id.to_string(); + s[..8.min(s.len())].to_string() + }; let folders = self .folder_storage - .list_folders_by_owner(parent_id, &owner_id_str) + .list_folders_by_owner(parent_id, owner_id) .await .map_err(|e| { DomainError::internal_error( @@ -287,10 +288,10 @@ impl FolderUseCase for FolderService { "No root folders found for user {}, creating home folder automatically", owner_id ); - let folder_name = format!("My Folder - {}", &owner_id_str[..8.min(owner_id_str.len())]); + let folder_name = format!("My Folder - {}", owner_id_short); match self .folder_storage - .create_home_folder(&owner_id_str, folder_name.clone()) + .create_home_folder(owner_id, folder_name.clone()) .await { Ok(home_folder) => { @@ -354,14 +355,13 @@ impl FolderUseCase for FolderService { pagination: &crate::application::dtos::pagination::PaginationRequestDto, ) -> Result, DomainError> { - let owner_id_str = owner_id.to_string(); let pagination = pagination.validate_and_adjust(); let (folders, total_items) = self .folder_storage .list_folders_by_owner_paginated( parent_id, - &owner_id_str, + owner_id, pagination.offset(), pagination.limit(), true, @@ -396,7 +396,6 @@ impl FolderUseCase for FolderService { dto: RenameFolderDto, caller_id: Uuid, ) -> Result { - let caller_id_str = caller_id.to_string(); // Input validation if dto.name.is_empty() { return Err(DomainError::new( @@ -414,7 +413,7 @@ impl FolderUseCase for FolderService { ) })?; - if existing_folder.owner_id() != Some(caller_id_str.as_str()) { + if existing_folder.owner_id() != Some(caller_id) { tracing::warn!( "rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'", caller_id, @@ -446,7 +445,6 @@ impl FolderUseCase for FolderService { dto: MoveFolderDto, caller_id: Uuid, ) -> Result { - let caller_id_str = caller_id.to_string(); // Verify the source folder exists and belongs to the caller let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| { DomainError::internal_error( @@ -455,7 +453,7 @@ impl FolderUseCase for FolderService { ) })?; - if source_folder.owner_id() != Some(caller_id_str.as_str()) { + if source_folder.owner_id() != Some(caller_id) { tracing::warn!( "move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'", caller_id, @@ -503,7 +501,6 @@ impl FolderUseCase for FolderService { /// Deletes a folder after verifying ownership. async fn delete_folder(&self, id: &str, caller_id: Uuid) -> Result<(), DomainError> { - let caller_id_str = caller_id.to_string(); // Verify the folder exists and belongs to the caller let folder = self.folder_storage.get_folder(id).await.map_err(|e| { DomainError::internal_error( @@ -512,7 +509,7 @@ impl FolderUseCase for FolderService { ) })?; - if folder.owner_id() != Some(caller_id_str.as_str()) { + if folder.owner_id() != Some(caller_id) { tracing::warn!( "delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'", caller_id, diff --git a/src/application/services/search_service.rs b/src/application/services/search_service.rs index 934723c2..c535226e 100755 --- a/src/application/services/search_service.rs +++ b/src/application/services/search_service.rs @@ -323,7 +323,7 @@ impl SearchUseCase for SearchService { .search_folders( criteria.folder_id.as_deref(), criteria.name_contains.as_deref(), - &user_id_str, + user_id, false, ) .await?; @@ -405,7 +405,7 @@ impl SearchUseCase for SearchService { .search_folders( criteria.folder_id.as_deref(), criteria.name_contains.as_deref(), - &user_id_str, + user_id, true, ) .await?; diff --git a/src/application/services/trash_service.rs b/src/application/services/trash_service.rs index 434a9f5f..1e375ffb 100755 --- a/src/application/services/trash_service.rs +++ b/src/application/services/trash_service.rs @@ -260,7 +260,7 @@ impl TrashUseCase for TrashService { // Ownership check — return NotFound (not Forbidden) to // prevent leaking whether the folder exists. - if folder.owner_id().is_none_or(|o| o != user_id.to_string()) { + if folder.owner_id() != Some(user_id) { return Err(DomainError::not_found( "Folder", format!("Folder not found: {}", item_id), diff --git a/src/common/stubs.rs b/src/common/stubs.rs index 341d9a0e..7b2170ff 100755 --- a/src/common/stubs.rs +++ b/src/common/stubs.rs @@ -246,7 +246,7 @@ impl FolderRepository for StubFolderStoragePort { async fn list_folders_by_owner( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, ) -> Result, DomainError> { Ok(Vec::new()) } @@ -264,7 +264,7 @@ impl FolderRepository for StubFolderStoragePort { async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, - _owner_id: &str, + _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, @@ -314,7 +314,7 @@ impl FolderRepository for StubFolderStoragePort { async fn create_home_folder( &self, - _user_id: &str, + _user_id: Uuid, _name: String, ) -> Result { Ok(Folder::default()) diff --git a/src/domain/entities/file.rs b/src/domain/entities/file.rs index 67d337b1..5dac1fc5 100755 --- a/src/domain/entities/file.rs +++ b/src/domain/entities/file.rs @@ -1,3 +1,5 @@ +use uuid::Uuid; + use crate::domain::services::path_service::StoragePath; // Re-export entity errors from the centralized module @@ -18,7 +20,7 @@ pub struct FileParts { pub folder_id: Option, pub created_at: u64, pub modified_at: u64, - pub owner_id: Option, + pub owner_id: Option, } /** @@ -61,7 +63,7 @@ pub struct File { modified_at: u64, /// Owner user ID (from storage.files.user_id) - owner_id: Option, + owner_id: Option, } // We no longer need this module, now we use a String directly @@ -161,7 +163,7 @@ impl File { folder_id: Option, created_at: u64, modified_at: u64, - owner_id: Option, + owner_id: Option, ) -> FileResult { // Validate file name if name.is_empty() || name.contains('/') || name.contains('\\') { @@ -241,8 +243,8 @@ impl File { self.modified_at } - pub fn owner_id(&self) -> Option<&str> { - self.owner_id.as_deref() + pub fn owner_id(&self) -> Option { + self.owner_id } #[allow(clippy::too_many_arguments)] diff --git a/src/domain/entities/folder.rs b/src/domain/entities/folder.rs index c719fac8..67d08035 100755 --- a/src/domain/entities/folder.rs +++ b/src/domain/entities/folder.rs @@ -1,3 +1,5 @@ +use uuid::Uuid; + use crate::domain::services::path_service::StoragePath; // Re-export entity errors from the centralized module @@ -23,7 +25,7 @@ pub struct Folder { /// Owner user ID — scopes folder visibility per user. /// `None` only for legacy/stub folders; real folders always have an owner. - owner_id: Option, + owner_id: Option, /// Creation timestamp created_at: u64, @@ -66,7 +68,7 @@ impl Folder { name: String, storage_path: StoragePath, parent_id: Option, - owner_id: Option, + owner_id: Option, ) -> FolderResult { // Validate folder name if name.is_empty() || name.contains('/') || name.contains('\\') { @@ -119,7 +121,7 @@ impl Folder { name: String, storage_path: StoragePath, parent_id: Option, - owner_id: Option, + owner_id: Option, created_at: u64, modified_at: u64, ) -> FolderResult { @@ -172,8 +174,8 @@ impl Folder { self.modified_at } - pub fn owner_id(&self) -> Option<&str> { - self.owner_id.as_deref() + pub fn owner_id(&self) -> Option { + self.owner_id } /// Creates a new Folder instance from a DTO diff --git a/src/domain/repositories/folder_repository.rs b/src/domain/repositories/folder_repository.rs index 4aa8daa2..0a641567 100755 --- a/src/domain/repositories/folder_repository.rs +++ b/src/domain/repositories/folder_repository.rs @@ -11,6 +11,7 @@ use crate::common::errors::DomainError; use crate::domain::entities::folder::Folder; use crate::domain::services::path_service::StoragePath; +use uuid::Uuid; /// Domain port for folder persistence. /// @@ -39,7 +40,7 @@ pub trait FolderRepository: Send + Sync + 'static { async fn list_folders_by_owner( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError>; /// Lists folders with pagination @@ -57,7 +58,7 @@ pub trait FolderRepository: Send + Sync + 'static { async fn list_folders_by_owner_paginated( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: usize, limit: usize, include_total: bool, @@ -99,7 +100,7 @@ pub trait FolderRepository: Send + Sync + 'static { /// Creates a root-level home folder for a user. /// This is used during user registration to create the user's personal folder. - async fn create_home_folder(&self, user_id: &str, name: String) -> Result; + async fn create_home_folder(&self, user_id: Uuid, name: String) -> Result; /// Lists every folder in a subtree rooted at `folder_id` (inclusive). /// @@ -123,7 +124,7 @@ pub trait FolderRepository: Send + Sync + 'static { &self, folder_id: &str, name_contains: Option<&str>, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let _ = (folder_id, name_contains, user_id); Ok(Vec::new()) @@ -144,7 +145,7 @@ pub trait FolderRepository: Send + Sync + 'static { &self, parent_id: Option<&str>, name_contains: Option<&str>, - user_id: &str, + user_id: Uuid, recursive: bool, ) -> Result, DomainError> { // Recursive with folder_id → use optimised ltree scan diff --git a/src/infrastructure/repositories/pg/file_blob_read_repository.rs b/src/infrastructure/repositories/pg/file_blob_read_repository.rs index bf6f2d00..013ca21e 100755 --- a/src/infrastructure/repositories/pg/file_blob_read_repository.rs +++ b/src/infrastructure/repositories/pg/file_blob_read_repository.rs @@ -17,7 +17,7 @@ type MediaFileRow = ( String, // mime_type i64, // created_at i64, // updated_at - Option, // user_id + Option, // user_id i64, // sort_date ); @@ -47,7 +47,7 @@ type FileRow = ( String, i64, i64, - Option, + Option, ); /// File read repository backed by PostgreSQL metadata + blob storage. @@ -114,7 +114,7 @@ impl FileBlobReadRepository { mime_type: String, created_at: i64, modified_at: i64, - owner_id: Option, + owner_id: Option, ) -> Result { let storage_path = Self::make_file_path(folder_path.as_deref(), &name); File::with_timestamps( @@ -177,7 +177,7 @@ impl FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text, + fi.user_id, EXTRACT(EPOCH FROM COALESCE(fm.captured_at, fi.created_at))::bigint AS sort_date FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id @@ -226,7 +226,7 @@ impl FileReadPort for FileBlobReadRepository { i64, // created_at i64, // updated_at String, // blob_hash - Option, // user_id (owner) + Option, // user_id (owner) ), >( r#" @@ -235,7 +235,7 @@ impl FileReadPort for FileBlobReadRepository { EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, fi.blob_hash, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.id = $1::uuid AND NOT fi.is_trashed @@ -269,7 +269,7 @@ impl FileReadPort for FileBlobReadRepository { i64, // created_at i64, // updated_at String, // blob_hash - Option, // user_id (owner) + Option, // user_id (owner) ), >( r#" @@ -278,7 +278,7 @@ impl FileReadPort for FileBlobReadRepository { EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, fi.blob_hash, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.id = $1::uuid @@ -310,7 +310,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed @@ -327,7 +327,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id IS NULL AND NOT fi.is_trashed @@ -360,7 +360,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed @@ -379,7 +379,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id IS NULL AND NOT fi.is_trashed @@ -422,7 +422,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed @@ -442,7 +442,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id IS NULL AND NOT fi.is_trashed @@ -480,7 +480,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id = $1::uuid AND NOT fi.is_trashed @@ -502,7 +502,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id IS NULL AND NOT fi.is_trashed @@ -640,7 +640,7 @@ impl FileReadPort for FileBlobReadRepository { String, i64, i64, - Option, + Option, ), >( r#" @@ -648,7 +648,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.name = $1 AND fi.folder_id IS NULL AND NOT fi.is_trashed @@ -670,7 +670,7 @@ impl FileReadPort for FileBlobReadRepository { String, i64, i64, - Option, + Option, ), >( r#" @@ -678,7 +678,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fo.path = $1 AND fi.name = $2 AND NOT fi.is_trashed @@ -713,14 +713,14 @@ impl FileReadPort for FileBlobReadRepository { let stream = async_stream::try_stream! { let mut row_stream = sqlx::query_as::<_, ( String, String, Option, Option, - i64, String, i64, i64, Option, + i64, String, i64, i64, Option, )>( r#" SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path, fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $1::uuid) @@ -772,7 +772,7 @@ impl FileReadPort for FileBlobReadRepository { // ── Build dynamic WHERE + bind indices ─────────────────────────── let mut conditions: Vec = vec![ - "fi.user_id = $1::uuid".to_string(), + "fi.user_id = $1".to_string(), "fi.is_trashed = false".to_string(), ]; let mut bind_idx = 1u32; // $1 = user_id @@ -798,7 +798,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, \ EXTRACT(EPOCH FROM fi.created_at)::bigint, \ EXTRACT(EPOCH FROM fi.updated_at)::bigint, \ - fi.user_id::text, \ + fi.user_id, \ COUNT(*) OVER() AS total_count \ FROM storage.files fi \ LEFT JOIN storage.folders fo ON fo.id = fi.folder_id \ @@ -819,7 +819,7 @@ impl FileReadPort for FileBlobReadRepository { String, i64, i64, - Option, + Option, i64, ), >(&sql) @@ -959,7 +959,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, \ EXTRACT(EPOCH FROM fi.created_at)::bigint, \ EXTRACT(EPOCH FROM fi.updated_at)::bigint, \ - fi.user_id::text, \ + fi.user_id, \ COUNT(*) OVER() AS total_count \ FROM storage.files fi \ JOIN storage.folders fo ON fo.id = fi.folder_id \ @@ -980,7 +980,7 @@ impl FileReadPort for FileBlobReadRepository { String, i64, i64, - Option, + Option, i64, ), >(&sql) @@ -1069,7 +1069,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id = $1::uuid @@ -1097,7 +1097,7 @@ impl FileReadPort for FileBlobReadRepository { fi.size, fi.mime_type, EXTRACT(EPOCH FROM fi.created_at)::bigint, EXTRACT(EPOCH FROM fi.updated_at)::bigint, - fi.user_id::text + fi.user_id FROM storage.files fi LEFT JOIN storage.folders fo ON fo.id = fi.folder_id WHERE fi.folder_id IS NULL diff --git a/src/infrastructure/repositories/pg/file_blob_write_repository.rs b/src/infrastructure/repositories/pg/file_blob_write_repository.rs index bb1c45de..5b9a0969 100755 --- a/src/infrastructure/repositories/pg/file_blob_write_repository.rs +++ b/src/infrastructure/repositories/pg/file_blob_write_repository.rs @@ -10,6 +10,7 @@ use sqlx::PgPool; use std::path::PathBuf; use std::sync::Arc; +use uuid::Uuid; use crate::application::ports::storage_ports::{CopyFolderTreeResult, FileWritePort}; use crate::common::errors::DomainError; @@ -98,7 +99,7 @@ impl FileBlobWriteRepository { mime_type: String, created_at: i64, modified_at: i64, - owner_id: Option, + owner_id: Option, ) -> Result { let storage_path = Self::make_file_path(folder_path.as_deref(), &name); File::with_timestamps( @@ -116,7 +117,7 @@ impl FileBlobWriteRepository { } /// Derive user_id from the parent folder, or error if folder_id is None. - async fn resolve_user_id(&self, folder_id: Option<&str>) -> Result { + async fn resolve_user_id(&self, folder_id: Option<&str>) -> Result { match folder_id { Some(fid) => self.folder_repo.get_folder_user_id(fid).await, None => Err(DomainError::internal_error( @@ -219,7 +220,7 @@ impl FileWritePort for FileBlobWriteRepository { let row = match sqlx::query_as::<_, (String, i64, i64)>( r#" INSERT INTO storage.files (name, folder_id, user_id, blob_hash, size, mime_type) - VALUES ($1, $2::uuid, $3::uuid, $4, $5, $6) + VALUES ($1, $2::uuid, $3, $4, $5, $6) RETURNING id::text, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint @@ -227,7 +228,7 @@ impl FileWritePort for FileBlobWriteRepository { ) .bind(&name) .bind(&folder_id) - .bind(&user_id) + .bind(user_id) .bind(&blob_hash) .bind(size as i64) .bind(&content_type) @@ -500,7 +501,7 @@ impl FileWritePort for FileBlobWriteRepository { let row = sqlx::query_as::<_, (String, i64, i64)>( r#" INSERT INTO storage.files (name, folder_id, user_id, blob_hash, size, mime_type) - VALUES ($1, $2::uuid, $3::uuid, $4, $5, $6) + VALUES ($1, $2::uuid, $3, $4, $5, $6) RETURNING id::text, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint @@ -508,7 +509,7 @@ impl FileWritePort for FileBlobWriteRepository { ) .bind(&name) .bind(&folder_id) - .bind(&user_id) + .bind(user_id) .bind(placeholder_hash) .bind(size as i64) .bind(&content_type) diff --git a/src/infrastructure/repositories/pg/folder_db_repository.rs b/src/infrastructure/repositories/pg/folder_db_repository.rs index ed2b309c..7bf76a85 100755 --- a/src/infrastructure/repositories/pg/folder_db_repository.rs +++ b/src/infrastructure/repositories/pg/folder_db_repository.rs @@ -10,6 +10,7 @@ use sqlx::PgPool; use std::sync::Arc; +use uuid::Uuid; use crate::common::errors::DomainError; use crate::domain::entities::folder::Folder; @@ -17,7 +18,7 @@ use crate::domain::repositories::folder_repository::FolderRepository; use crate::domain::services::path_service::StoragePath; /// Type alias for folder metadata rows from SQL queries. -type FolderRow = (String, String, String, Option, String, i64, i64); +type FolderRow = (String, String, String, Option, Uuid, i64, i64); /// Type alias for paginated folder rows (includes total_count). type FolderRowPaginated = ( @@ -25,7 +26,7 @@ type FolderRowPaginated = ( String, String, Option, - String, + Uuid, i64, i64, i64, @@ -37,7 +38,7 @@ type FolderRowOptUser = ( String, String, Option, - Option, + Option, i64, i64, ); @@ -79,7 +80,7 @@ impl FolderDbRepository { name: String, path: String, parent_id: Option, - user_id: Option, + user_id: Option, created_at: i64, modified_at: i64, ) -> Result { @@ -106,9 +107,9 @@ impl FolderRepository for FolderDbRepository { // Derive user_id from parent folder. Root-level folders require the // caller to have set up the home folder beforehand (done during user // registration). - let user_id: String = if let Some(ref pid) = parent_id { - sqlx::query_scalar::<_, String>( - "SELECT user_id::text FROM storage.folders WHERE id = $1::uuid", + let user_id: Uuid = if let Some(ref pid) = parent_id { + sqlx::query_scalar::<_, Uuid>( + "SELECT user_id FROM storage.folders WHERE id = $1::uuid", ) .bind(pid) .fetch_optional(self.pool()) @@ -125,7 +126,7 @@ impl FolderRepository for FolderDbRepository { let row = sqlx::query_as::<_, (String, String, i64, i64)>( r#" INSERT INTO storage.folders (name, parent_id, user_id) - VALUES ($1, $2::uuid, $3::uuid) + VALUES ($1, $2::uuid, $3) RETURNING id::text, path, EXTRACT(EPOCH FROM created_at)::bigint, @@ -134,7 +135,7 @@ impl FolderRepository for FolderDbRepository { ) .bind(&name) .bind(&parent_id) - .bind(&user_id) + .bind(user_id) .fetch_one(self.pool()) .await .map_err(|e| { @@ -153,9 +154,9 @@ impl FolderRepository for FolderDbRepository { } async fn get_folder(&self, id: &str) -> Result { - let row = sqlx::query_as::<_, (String, String, String, Option, String, i64, i64)>( + let row = sqlx::query_as::<_, (String, String, String, Option, Uuid, i64, i64)>( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -180,9 +181,9 @@ impl FolderRepository for FolderDbRepository { return Err(DomainError::not_found("Folder", "empty path")); } - let row = sqlx::query_as::<_, (String, String, String, Option, String, i64, i64)>( + let row = sqlx::query_as::<_, (String, String, String, Option, Uuid, i64, i64)>( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -203,7 +204,7 @@ impl FolderRepository for FolderDbRepository { let rows: Vec = if let Some(pid) = parent_id { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -217,7 +218,7 @@ impl FolderRepository for FolderDbRepository { } else { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -241,16 +242,16 @@ impl FolderRepository for FolderDbRepository { async fn list_folders_by_owner( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, ) -> Result, DomainError> { let rows: Vec = if let Some(pid) = parent_id { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders - WHERE parent_id = $1::uuid AND user_id = $2::uuid AND NOT is_trashed + WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed ORDER BY name "#, ) @@ -261,11 +262,11 @@ impl FolderRepository for FolderDbRepository { } else { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders - WHERE parent_id IS NULL AND user_id = $1::uuid AND NOT is_trashed + WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed ORDER BY name "#, ) @@ -296,7 +297,7 @@ impl FolderRepository for FolderDbRepository { let rows: Vec = if let Some(pid) = parent_id { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, COUNT(*) OVER() AS total_count @@ -314,7 +315,7 @@ impl FolderRepository for FolderDbRepository { } else { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, COUNT(*) OVER() AS total_count @@ -353,7 +354,7 @@ impl FolderRepository for FolderDbRepository { async fn list_folders_by_owner_paginated( &self, parent_id: Option<&str>, - owner_id: &str, + owner_id: Uuid, offset: usize, limit: usize, include_total: bool, @@ -361,12 +362,12 @@ impl FolderRepository for FolderDbRepository { let rows: Vec = if let Some(pid) = parent_id { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, COUNT(*) OVER() AS total_count FROM storage.folders - WHERE parent_id = $1::uuid AND user_id = $2::uuid AND NOT is_trashed + WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed ORDER BY name LIMIT $3 OFFSET $4 "#, @@ -380,12 +381,12 @@ impl FolderRepository for FolderDbRepository { } else { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint, COUNT(*) OVER() AS total_count FROM storage.folders - WHERE parent_id IS NULL AND user_id = $1::uuid AND NOT is_trashed + WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed ORDER BY name LIMIT $2 OFFSET $3 "#, @@ -417,12 +418,12 @@ impl FolderRepository for FolderDbRepository { // The BEFORE UPDATE trigger recomputes path/lpath for this row; // the AFTER UPDATE cascade trigger then batch-updates all // descendants in a single UPDATE using the GiST lpath index. - let row = sqlx::query_as::<_, (String, String, String, Option, String, i64, i64)>( + let row = sqlx::query_as::<_, (String, String, String, Option, Uuid, i64, i64)>( r#" UPDATE storage.folders SET name = $1, updated_at = NOW() WHERE id = $2::uuid AND NOT is_trashed - RETURNING id::text, name, path, parent_id::text, user_id::text, + RETURNING id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint "#, @@ -452,12 +453,12 @@ impl FolderRepository for FolderDbRepository { // The BEFORE UPDATE trigger recomputes path/lpath for this row; // the AFTER UPDATE cascade trigger then batch-updates all // descendants in a single UPDATE using the GiST lpath index. - let row = sqlx::query_as::<_, (String, String, String, Option, String, i64, i64)>( + let row = sqlx::query_as::<_, (String, String, String, Option, Uuid, i64, i64)>( r#" UPDATE storage.folders SET parent_id = $1::uuid, updated_at = NOW() WHERE id = $2::uuid AND NOT is_trashed - RETURNING id::text, name, path, parent_id::text, user_id::text, + RETURNING id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint "#, @@ -627,11 +628,11 @@ impl FolderRepository for FolderDbRepository { Ok(()) } - async fn create_home_folder(&self, user_id: &str, name: String) -> Result { + async fn create_home_folder(&self, user_id: Uuid, name: String) -> Result { let row = sqlx::query_as::<_, (String, String, i64, i64)>( r#" INSERT INTO storage.folders (name, parent_id, user_id) - VALUES ($1, NULL, $2::uuid) + VALUES ($1, NULL, $2) ON CONFLICT DO NOTHING RETURNING id::text, path, @@ -651,7 +652,7 @@ impl FolderRepository for FolderDbRepository { name.clone(), path, None, - Some(user_id.to_string()), + Some(user_id), ca, ma, ), @@ -664,7 +665,7 @@ impl FolderRepository for FolderDbRepository { EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders - WHERE name = $1 AND user_id = $2::uuid AND parent_id IS NULL + WHERE name = $1 AND user_id = $2 AND parent_id IS NULL "#, ) .bind(&name) @@ -677,7 +678,7 @@ impl FolderRepository for FolderDbRepository { name, existing.1, None, - Some(user_id.to_string()), + Some(user_id), existing.2, existing.3, ) @@ -692,7 +693,7 @@ impl FolderRepository for FolderDbRepository { #[allow(clippy::type_complexity)] async fn list_subtree_folders(&self, folder_id: &str) -> Result, DomainError> { let sql = "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \ - fo.user_id::text, \ + fo.user_id, \ EXTRACT(EPOCH FROM fo.created_at)::bigint, \ EXTRACT(EPOCH FROM fo.updated_at)::bigint \ FROM storage.folders fo \ @@ -726,7 +727,7 @@ impl FolderRepository for FolderDbRepository { &self, parent_id: Option<&str>, name_contains: Option<&str>, - user_id: &str, + user_id: Uuid, recursive: bool, ) -> Result, DomainError> { // Recursive with folder scope → existing optimised ltree scan @@ -754,11 +755,11 @@ impl FolderRepository for FolderDbRepository { // Recursive, no folder scope → ALL user folders let sql = format!( "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \ - fo.user_id::text, \ + fo.user_id, \ EXTRACT(EPOCH FROM fo.created_at)::bigint, \ EXTRACT(EPOCH FROM fo.updated_at)::bigint \ FROM storage.folders fo \ - WHERE fo.user_id = $1::uuid \ + WHERE fo.user_id = $1 \ AND fo.is_trashed = false \ {name_clause} \ ORDER BY fo.name" @@ -790,12 +791,12 @@ impl FolderRepository for FolderDbRepository { let sql = if parent_id.is_some() { format!( "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \ - fo.user_id::text, \ + fo.user_id, \ EXTRACT(EPOCH FROM fo.created_at)::bigint, \ EXTRACT(EPOCH FROM fo.updated_at)::bigint \ FROM storage.folders fo \ WHERE fo.parent_id = $1::uuid \ - AND fo.user_id = $2::uuid \ + AND fo.user_id = $2 \ AND fo.is_trashed = false \ {name_clause} \ ORDER BY fo.name" @@ -808,12 +809,12 @@ impl FolderRepository for FolderDbRepository { }; format!( "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \ - fo.user_id::text, \ + fo.user_id, \ EXTRACT(EPOCH FROM fo.created_at)::bigint, \ EXTRACT(EPOCH FROM fo.updated_at)::bigint \ FROM storage.folders fo \ WHERE fo.parent_id IS NULL \ - AND fo.user_id = $1::uuid \ + AND fo.user_id = $1 \ AND fo.is_trashed = false \ {name_clause_root} \ ORDER BY fo.name" @@ -865,7 +866,7 @@ impl FolderRepository for FolderDbRepository { &self, folder_id: &str, name_contains: Option<&str>, - user_id: &str, + user_id: Uuid, ) -> Result, DomainError> { let (where_extra, name_pattern) = match name_contains { Some(name) if name.len() >= 3 => { @@ -876,11 +877,11 @@ impl FolderRepository for FolderDbRepository { let sql = format!( "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \ - fo.user_id::text, \ + fo.user_id, \ EXTRACT(EPOCH FROM fo.created_at)::bigint, \ EXTRACT(EPOCH FROM fo.updated_at)::bigint \ FROM storage.folders fo \ - WHERE fo.user_id = $1::uuid \ + WHERE fo.user_id = $1 \ AND fo.is_trashed = false \ AND fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $2::uuid) \ AND fo.id != $2::uuid \ @@ -924,7 +925,7 @@ impl FolderRepository for FolderDbRepository { let rows: Vec = if let Some(pid) = parent_id { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -949,7 +950,7 @@ impl FolderRepository for FolderDbRepository { } else { sqlx::query_as( r#" - SELECT id::text, name, path, parent_id::text, user_id::text, + SELECT id::text, name, path, parent_id::text, user_id, EXTRACT(EPOCH FROM created_at)::bigint, EXTRACT(EPOCH FROM updated_at)::bigint FROM storage.folders @@ -985,8 +986,8 @@ impl FolderRepository for FolderDbRepository { impl FolderDbRepository { /// Returns user_id for a given folder. Used by file repositories. - pub async fn get_folder_user_id(&self, folder_id: &str) -> Result { - sqlx::query_scalar::<_, String>("SELECT user_id::text FROM storage.folders WHERE id = $1::uuid") + pub async fn get_folder_user_id(&self, folder_id: &str) -> Result { + sqlx::query_scalar::<_, Uuid>("SELECT user_id FROM storage.folders WHERE id = $1::uuid") .bind(folder_id) .fetch_optional(self.pool()) .await