1use docbox_database::{
2 DbErr, DbPool, DbResult, DbTransaction,
3 models::{
4 document_box::DocumentBoxScopeRaw,
5 edit_history::{
6 CreateEditHistory, CreateEditHistoryType, EditHistory, EditHistoryMetadata,
7 },
8 folder::{Folder, FolderId},
9 link::{Link, LinkId},
10 user::UserId,
11 },
12};
13use docbox_search::{SearchError, TenantSearchIndex, models::UpdateSearchIndexData};
14use std::ops::DerefMut;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum UpdateLinkError {
19 #[error(transparent)]
21 Database(#[from] DbErr),
22
23 #[error("unknown target folder")]
25 UnknownTargetFolder,
26
27 #[error(transparent)]
29 SearchIndex(SearchError),
30}
31
32pub struct UpdateLink {
33 pub folder_id: Option<FolderId>,
35
36 pub name: Option<String>,
38
39 pub value: Option<String>,
41
42 pub pinned: Option<bool>,
44}
45
46pub async fn update_link(
47 db: &DbPool,
48 search: &TenantSearchIndex,
49 scope: &DocumentBoxScopeRaw,
50 link: Link,
51 user_id: Option<String>,
52 update: UpdateLink,
53) -> Result<(), UpdateLinkError> {
54 let mut link = link;
55
56 let mut db = db
57 .begin()
58 .await
59 .inspect_err(|error| tracing::error!(?error, "failed to begin transaction"))?;
60
61 if let Some(target_id) = update.folder_id {
62 let target_folder = Folder::find_by_id(db.deref_mut(), scope, target_id)
65 .await
66 .inspect_err(|error| tracing::error!(?error, "failed to query target folder"))?
67 .ok_or(UpdateLinkError::UnknownTargetFolder)?;
68
69 link = move_link(&mut db, user_id.clone(), link, target_folder)
70 .await
71 .inspect_err(|error| tracing::error!(?error, "failed to move link"))?;
72 };
73
74 if let Some(new_name) = update.name {
75 link = update_link_name(&mut db, user_id.clone(), link, new_name)
76 .await
77 .inspect_err(|error| tracing::error!(?error, "failed to update link name"))?;
78 }
79
80 if let Some(new_value) = update.value {
81 link = update_link_value(&mut db, user_id.clone(), link, new_value)
82 .await
83 .inspect_err(|error| tracing::error!(?error, "failed to update link value"))?;
84 }
85
86 if let Some(new_value) = update.pinned {
87 link = update_link_pinned(&mut db, user_id, link, new_value)
88 .await
89 .inspect_err(|error| tracing::error!(?error, "failed to update link pinned state"))?;
90 }
91
92 search
94 .update_data(
95 link.id,
96 UpdateSearchIndexData {
97 folder_id: link.folder_id,
98 name: link.name.clone(),
99 content: Some(link.value.clone()),
100 pages: None,
101 },
102 )
103 .await
104 .inspect_err(|error| tracing::error!(?error, "failed to update search index"))
105 .map_err(UpdateLinkError::SearchIndex)?;
106
107 db.commit()
108 .await
109 .inspect_err(|error| tracing::error!(?error, "failed to commit transaction"))?;
110
111 Ok(())
112}
113
114#[tracing::instrument(skip_all, fields(?user_id, %link_id, ?metadata))]
116async fn add_edit_history(
117 db: &mut DbTransaction<'_>,
118 user_id: Option<UserId>,
119 link_id: LinkId,
120 metadata: EditHistoryMetadata,
121) -> DbResult<()> {
122 EditHistory::create(
123 db.deref_mut(),
124 CreateEditHistory {
125 ty: CreateEditHistoryType::Link(link_id),
126 user_id,
127 metadata,
128 },
129 )
130 .await
131 .inspect_err(|error| tracing::error!(?error, "failed to store link edit history entry"))?;
132
133 Ok(())
134}
135
136#[tracing::instrument(skip_all, fields(?user_id, link_id = %link.id, target_folder_id = %target_folder.id))]
139async fn move_link(
140 db: &mut DbTransaction<'_>,
141 user_id: Option<UserId>,
142 link: Link,
143 target_folder: Folder,
144) -> DbResult<Link> {
145 add_edit_history(
147 db,
148 user_id,
149 link.id,
150 EditHistoryMetadata::MoveToFolder {
151 original_id: link.folder_id,
152 target_id: target_folder.id,
153 },
154 )
155 .await?;
156
157 link.move_to_folder(db.deref_mut(), target_folder.id)
158 .await
159 .inspect_err(|error| tracing::error!(?error, "failed to move link"))
160}
161
162#[tracing::instrument(skip_all, fields(?user_id, link_id = %link.id, %new_value))]
165async fn update_link_value(
166 db: &mut DbTransaction<'_>,
167 user_id: Option<UserId>,
168 link: Link,
169 new_value: String,
170) -> DbResult<Link> {
171 add_edit_history(
173 db,
174 user_id,
175 link.id,
176 EditHistoryMetadata::LinkValue {
177 previous_value: link.value.clone(),
178 new_value: new_value.clone(),
179 },
180 )
181 .await?;
182
183 link.update_value(db.deref_mut(), new_value)
184 .await
185 .inspect_err(|error| tracing::error!(?error, "failed to update link value"))
186}
187
188#[tracing::instrument(skip_all, fields(?user_id, link_id = %link.id, %new_value))]
191async fn update_link_pinned(
192 db: &mut DbTransaction<'_>,
193 user_id: Option<UserId>,
194 link: Link,
195 new_value: bool,
196) -> DbResult<Link> {
197 add_edit_history(
199 db,
200 user_id,
201 link.id,
202 EditHistoryMetadata::ChangePinned {
203 previous_value: link.pinned,
204 new_value,
205 },
206 )
207 .await?;
208
209 link.set_pinned(db.deref_mut(), new_value)
210 .await
211 .inspect_err(|error| tracing::error!(?error, "failed to update link pinned state"))
212}
213
214#[tracing::instrument(skip_all, fields(?user_id, link_id = %link.id, %new_name))]
217async fn update_link_name(
218 db: &mut DbTransaction<'_>,
219 user_id: Option<UserId>,
220 link: Link,
221 new_name: String,
222) -> DbResult<Link> {
223 add_edit_history(
225 db,
226 user_id,
227 link.id,
228 EditHistoryMetadata::Rename {
229 original_name: link.name.clone(),
230 new_name: new_name.clone(),
231 },
232 )
233 .await?;
234
235 link.rename(db.deref_mut(), new_name)
236 .await
237 .inspect_err(|error| tracing::error!(?error, "failed to rename link"))
238}