docbox_core/links/
update_link.rs

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::{TenantSearchIndex, models::UpdateSearchIndexData};
14use std::ops::DerefMut;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum UpdateLinkError {
19    /// Database related error
20    #[error(transparent)]
21    Database(#[from] DbErr),
22
23    /// Target folder could not be found
24    #[error("unknown target folder")]
25    UnknownTargetFolder,
26
27    /// Failed to update the search index
28    #[error(transparent)]
29    SearchIndex(anyhow::Error),
30}
31
32pub struct UpdateLink {
33    /// Move the link to another folder
34    pub folder_id: Option<FolderId>,
35
36    /// Update the link name
37    pub name: Option<String>,
38
39    /// Update the link value
40    pub value: Option<String>,
41
42    /// Update the pinned state
43    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        // Ensure the target folder exists, also ensures the target folder is in the same scope
63        // (We may allow across scopes in the future, but would need additional checks for access control of target scope)
64        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    // Update search index data for the new name and value
93    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/// Add a new edit history item for a link
115#[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/// Moves a link to the provided folder, creates a new edit history
137/// item for the change
138#[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    // Track the edit history
146    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/// Updates a link value, creates a new edit history
163/// item for the change
164#[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    // Track the edit history
172    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/// Updates a link pinned state, creates a new edit history
189/// item for the change
190#[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    // Track the edit history
198    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/// Updates a link name, creates a new edit history
215/// item for the change
216#[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    // Track the edit history
224    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}