use docbox_database::{
DbErr, DbPool, DbResult, DbTransaction,
models::{
document_box::DocumentBoxScopeRaw,
edit_history::{
CreateEditHistory, CreateEditHistoryType, EditHistory, EditHistoryMetadata,
},
folder::{Folder, FolderId},
shared::WithFullPath,
user::UserId,
},
};
use docbox_search::{SearchError, TenantSearchIndex, models::UpdateSearchIndexData};
use std::ops::DerefMut;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UpdateFolderError {
#[error(transparent)]
Database(#[from] DbErr),
#[error("unknown target folder")]
UnknownTargetFolder,
#[error("cannot modify root")]
CannotModifyRoot,
#[error("cannot move into self")]
CannotMoveIntoSelf,
#[error("cannot move into child of self")]
CannotMoveIntoChildOfSelf,
#[error(transparent)]
SearchIndex(SearchError),
}
pub struct UpdateFolder {
pub folder_id: Option<FolderId>,
pub name: Option<String>,
pub pinned: Option<bool>,
}
pub async fn update_folder(
db: &DbPool,
search: &TenantSearchIndex,
scope: &DocumentBoxScopeRaw,
folder: Folder,
user_id: Option<String>,
update: UpdateFolder,
) -> Result<(), UpdateFolderError> {
let mut folder = folder;
let mut folder_id = folder
.folder_id
.ok_or(UpdateFolderError::CannotModifyRoot)?;
let mut db = db
.begin()
.await
.inspect_err(|error| tracing::error!(?error, "failed to begin transaction"))?;
if let Some(target_id) = update.folder_id {
if target_id == folder.id {
return Err(UpdateFolderError::CannotMoveIntoSelf);
}
let WithFullPath {
data: target_folder,
full_path: target_folder_path,
} = Folder::find_by_id_with_extra(db.deref_mut(), scope, target_id)
.await
.inspect_err(|error| tracing::error!(?error, "failed to query target folder"))?
.ok_or(UpdateFolderError::UnknownTargetFolder)?;
if target_folder_path
.iter()
.any(|segment| segment.id == folder.id)
{
return Err(UpdateFolderError::CannotMoveIntoChildOfSelf);
}
folder_id = target_folder.folder.id;
folder = move_folder(
&mut db,
user_id.clone(),
folder,
folder_id,
target_folder.folder,
)
.await
.inspect_err(|error| tracing::error!(?error, "failed to move folder"))?;
};
if let Some(new_name) = update.name {
folder = update_folder_name(&mut db, user_id.clone(), folder, new_name)
.await
.inspect_err(|error| tracing::error!(?error, "failed to update folder name"))?;
}
if let Some(new_value) = update.pinned {
folder = update_folder_pinned(&mut db, user_id, folder, new_value)
.await
.inspect_err(|error| tracing::error!(?error, "failed to update folder pinned state"))?;
}
search
.update_data(
folder.id,
UpdateSearchIndexData {
folder_id,
name: folder.name.clone(),
content: None,
pages: None,
},
)
.await
.map_err(|error| {
tracing::error!(?error, "failed to update search index");
UpdateFolderError::SearchIndex(error)
})?;
db.commit().await.inspect_err(|error| {
tracing::error!(?error, "failed to commit transaction");
})?;
Ok(())
}
#[tracing::instrument(skip_all, fields(?user_id, %folder_id, ?metadata))]
async fn add_edit_history(
db: &mut DbTransaction<'_>,
user_id: Option<UserId>,
folder_id: FolderId,
metadata: EditHistoryMetadata,
) -> DbResult<()> {
EditHistory::create(
db.deref_mut(),
CreateEditHistory {
ty: CreateEditHistoryType::Folder(folder_id),
user_id,
metadata,
},
)
.await
.inspect_err(|error| tracing::error!(?error, "failed to store folder edit history entry"))?;
Ok(())
}
#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, target_folder_id = %target_folder.id))]
async fn move_folder(
db: &mut DbTransaction<'_>,
user_id: Option<UserId>,
folder: Folder,
folder_id: FolderId,
target_folder: Folder,
) -> DbResult<Folder> {
add_edit_history(
db,
user_id,
folder.id,
EditHistoryMetadata::MoveToFolder {
original_id: folder_id,
target_id: target_folder.id,
},
)
.await?;
folder
.move_to_folder(db.deref_mut(), target_folder.id)
.await
.inspect_err(|error| tracing::error!(?error, "failed to move folder"))
}
#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_value))]
async fn update_folder_pinned(
db: &mut DbTransaction<'_>,
user_id: Option<UserId>,
folder: Folder,
new_value: bool,
) -> DbResult<Folder> {
add_edit_history(
db,
user_id,
folder.id,
EditHistoryMetadata::ChangePinned {
previous_value: folder.pinned,
new_value,
},
)
.await?;
folder
.set_pinned(db.deref_mut(), new_value)
.await
.inspect_err(|error| tracing::error!(?error, "failed to update folder pinned state"))
}
#[tracing::instrument(skip_all, fields(?user_id, folder_id = %folder.id, %new_name))]
async fn update_folder_name(
db: &mut DbTransaction<'_>,
user_id: Option<UserId>,
folder: Folder,
new_name: String,
) -> DbResult<Folder> {
add_edit_history(
db,
user_id,
folder.id,
EditHistoryMetadata::Rename {
original_name: folder.name.clone(),
new_name: new_name.clone(),
},
)
.await?;
folder
.rename(db.deref_mut(), new_name)
.await
.inspect_err(|error| tracing::error!(?error, "failed to rename folder in database"))
}