docbox_database/models/
edit_history.rs

1//! Database structure that tracks changes to files
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sqlx::FromRow;
6use std::str::FromStr;
7use utoipa::ToSchema;
8use uuid::Uuid;
9
10use super::{file::FileId, folder::FolderId, user::UserId};
11use crate::models::link::LinkId;
12use crate::models::user::User;
13use crate::{DbErr, DbExecutor, DbResult};
14
15pub type EditHistoryId = Uuid;
16
17#[derive(
18    Debug,
19    Clone,
20    Copy,
21    strum::EnumString,
22    strum::Display,
23    Deserialize,
24    Serialize,
25    ToSchema,
26    PartialEq,
27    Eq,
28)]
29pub enum EditHistoryType {
30    /// File was moved to a different folder
31    MoveToFolder,
32    /// File was renamed
33    Rename,
34    /// Link value was changed
35    LinkValue,
36    /// Pinned state changed
37    ChangePinned,
38}
39
40impl TryFrom<String> for EditHistoryType {
41    type Error = strum::ParseError;
42    fn try_from(value: String) -> Result<Self, Self::Error> {
43        EditHistoryType::from_str(&value)
44    }
45}
46
47/// Metadata associated with an edit history
48#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq, Eq)]
49#[serde(tag = "type")]
50pub enum EditHistoryMetadata {
51    MoveToFolder {
52        /// Folder moved from
53        #[schema(value_type = Uuid)]
54        original_id: FolderId,
55        /// Folder moved to
56        #[schema(value_type = Uuid)]
57        target_id: FolderId,
58    },
59
60    Rename {
61        /// Previous name
62        original_name: String,
63        /// New name
64        new_name: String,
65    },
66
67    LinkValue {
68        /// Previous URL
69        previous_value: String,
70        /// New URL
71        new_value: String,
72    },
73
74    ChangePinned {
75        // Previous pinned state
76        previous_value: bool,
77
78        // New pinned state
79        new_value: bool,
80    },
81}
82
83#[derive(Debug, Serialize, FromRow, ToSchema)]
84pub struct EditHistory {
85    /// Unique identifier for this history entry
86    #[schema(value_type = Uuid)]
87    pub id: EditHistoryId,
88
89    /// ID of the file that was edited (If a file was edited)
90    #[serde(skip_serializing_if = "Option::is_none")]
91    #[schema(value_type = Option<Uuid>)]
92    pub file_id: Option<FileId>,
93    /// ID of the file that was edited (If a link was edited)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    #[schema(value_type = Option<Uuid>)]
96    pub link_id: Option<LinkId>,
97    /// ID of the file that was edited (If a folder was edited)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    #[schema(value_type = Option<Uuid>)]
100    pub folder_id: Option<FolderId>,
101
102    /// User that made the edit
103    pub user: Option<User>,
104
105    /// The type of change that was made
106    #[serde(rename = "type")]
107    #[sqlx(rename = "type")]
108    #[sqlx(try_from = "String")]
109    pub ty: EditHistoryType,
110
111    /// Metadata associated with the change
112    #[schema(value_type = EditHistoryMetadata)]
113    pub metadata: sqlx::types::Json<EditHistoryMetadata>,
114
115    /// When this change was made
116    pub created_at: DateTime<Utc>,
117}
118
119pub struct CreateEditHistory {
120    pub ty: CreateEditHistoryType,
121    pub user_id: Option<UserId>,
122    pub metadata: EditHistoryMetadata,
123}
124
125#[derive(PartialEq, Eq)]
126pub enum CreateEditHistoryType {
127    File(FileId),
128    Folder(FolderId),
129    Link(LinkId),
130}
131
132impl EditHistory {
133    pub async fn create(
134        db: impl DbExecutor<'_>,
135        CreateEditHistory {
136            ty,
137            user_id,
138            metadata,
139        }: CreateEditHistory,
140    ) -> DbResult<()> {
141        let id = Uuid::new_v4();
142        let created_at = Utc::now();
143
144        let mut file_id: Option<FileId> = None;
145        let mut folder_id: Option<FolderId> = None;
146        let mut link_id: Option<LinkId> = None;
147
148        match ty {
149            CreateEditHistoryType::File(id) => file_id = Some(id),
150            CreateEditHistoryType::Folder(id) => folder_id = Some(id),
151            CreateEditHistoryType::Link(id) => link_id = Some(id),
152        }
153
154        let ty = match &metadata {
155            EditHistoryMetadata::MoveToFolder { .. } => EditHistoryType::MoveToFolder,
156            EditHistoryMetadata::Rename { .. } => EditHistoryType::Rename,
157            EditHistoryMetadata::LinkValue { .. } => EditHistoryType::LinkValue,
158            EditHistoryMetadata::ChangePinned { .. } => EditHistoryType::ChangePinned,
159        };
160
161        let metadata = serde_json::to_value(&metadata).map_err(|err| DbErr::Encode(err.into()))?;
162
163        sqlx::query(
164            r#"
165            INSERT INTO "docbox_edit_history" (
166                "id", "file_id", "link_id",
167                "folder_id", "user_id", "type",
168                "metadata", "created_at"
169            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
170        "#,
171        )
172        .bind(id)
173        .bind(file_id)
174        .bind(link_id)
175        .bind(folder_id)
176        .bind(user_id)
177        .bind(ty.to_string())
178        .bind(metadata)
179        .bind(created_at)
180        .execute(db)
181        .await?;
182
183        Ok(())
184    }
185
186    pub async fn all_by_file(
187        db: impl DbExecutor<'_>,
188        file_id: FileId,
189    ) -> DbResult<Vec<EditHistory>> {
190        sqlx::query_as(
191            r#"
192            SELECT "history".*, mk_docbox_user("user") AS "user"
193            FROM "docbox_edit_history" "history"
194            LEFT JOIN "docbox_users" "user" ON "history"."user_id" = "user"."id"
195            WHERE "history"."file_id" = $1
196            ORDER BY "history"."created_at" DESC
197        "#,
198        )
199        .bind(file_id)
200        .fetch_all(db)
201        .await
202    }
203
204    pub async fn all_by_folder(
205        db: impl DbExecutor<'_>,
206        folder_id: FolderId,
207    ) -> DbResult<Vec<EditHistory>> {
208        sqlx::query_as(
209            r#"
210            SELECT "history".*, mk_docbox_user("user") AS "user"
211            FROM "docbox_edit_history" "history"
212            LEFT JOIN "docbox_users" "user" ON "history"."user_id" = "user"."id"
213            WHERE "history"."folder_id" = $1
214            ORDER BY "history"."created_at" DESC
215        "#,
216        )
217        .bind(folder_id)
218        .fetch_all(db)
219        .await
220    }
221
222    pub async fn all_by_link(
223        db: impl DbExecutor<'_>,
224        link_id: LinkId,
225    ) -> DbResult<Vec<EditHistory>> {
226        sqlx::query_as(
227            r#"
228            SELECT "history".*, mk_docbox_user("user") AS "user"
229            FROM "docbox_edit_history" "history"
230            LEFT JOIN "docbox_users" "user" ON "history"."user_id" = "user"."id"
231            WHERE "history"."link_id" = $1
232            ORDER BY "history"."created_at" DESC
233        "#,
234        )
235        .bind(link_id)
236        .fetch_all(db)
237        .await
238    }
239}