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