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:
Diocrafts
2026-03-07 18:05:52 +01:00
parent cc98c5b0da
commit 6b02c10942
13 changed files with 131 additions and 129 deletions
@@ -17,7 +17,7 @@ type MediaFileRow = (
String, // mime_type
i64, // created_at
i64, // updated_at
Option<String>, // user_id
Option<Uuid>, // user_id
i64, // sort_date
);
@@ -47,7 +47,7 @@ type FileRow = (
String,
i64,
i64,
Option<String>,
Option<Uuid>,
);
/// 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<String>,
owner_id: Option<Uuid>,
) -> Result<File, DomainError> {
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<String>, // user_id (owner)
Option<Uuid>, // 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<String>, // user_id (owner)
Option<Uuid>, // 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<String>,
Option<Uuid>,
),
>(
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<String>,
Option<Uuid>,
),
>(
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<String>, Option<String>,
i64, String, i64, i64, Option<String>,
i64, String, i64, i64, Option<Uuid>,
)>(
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<String> = 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<String>,
Option<Uuid>,
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<String>,
Option<Uuid>,
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
@@ -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<String>,
owner_id: Option<Uuid>,
) -> Result<File, DomainError> {
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<String, DomainError> {
async fn resolve_user_id(&self, folder_id: Option<&str>) -> Result<Uuid, DomainError> {
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)
@@ -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>, String, i64, i64);
type FolderRow = (String, String, String, Option<String>, Uuid, i64, i64);
/// Type alias for paginated folder rows (includes total_count).
type FolderRowPaginated = (
@@ -25,7 +26,7 @@ type FolderRowPaginated = (
String,
String,
Option<String>,
String,
Uuid,
i64,
i64,
i64,
@@ -37,7 +38,7 @@ type FolderRowOptUser = (
String,
String,
Option<String>,
Option<String>,
Option<Uuid>,
i64,
i64,
);
@@ -79,7 +80,7 @@ impl FolderDbRepository {
name: String,
path: String,
parent_id: Option<String>,
user_id: Option<String>,
user_id: Option<Uuid>,
created_at: i64,
modified_at: i64,
) -> Result<Folder, DomainError> {
@@ -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<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#"
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>, String, i64, i64)>(
let row = sqlx::query_as::<_, (String, String, String, Option<String>, 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<FolderRow> = 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<Vec<Folder>, DomainError> {
let rows: Vec<FolderRow> = 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<FolderRowPaginated> = 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<FolderRowPaginated> = 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>, String, i64, i64)>(
let row = sqlx::query_as::<_, (String, String, String, Option<String>, 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>, String, i64, i64)>(
let row = sqlx::query_as::<_, (String, String, String, Option<String>, 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<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)>(
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<Vec<Folder>, 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<Vec<Folder>, 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<Vec<Folder>, 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<FolderRow> = 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<String, DomainError> {
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<Uuid, DomainError> {
sqlx::query_scalar::<_, Uuid>("SELECT user_id FROM storage.folders WHERE id = $1::uuid")
.bind(folder_id)
.fetch_optional(self.pool())
.await