1use 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 MoveToFolder,
32 Rename,
34 LinkValue,
36 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#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq, Eq)]
49#[serde(tag = "type")]
50pub enum EditHistoryMetadata {
51 MoveToFolder {
52 #[schema(value_type = Uuid)]
54 original_id: FolderId,
55 #[schema(value_type = Uuid)]
57 target_id: FolderId,
58 },
59
60 Rename {
61 original_name: String,
63 new_name: String,
65 },
66
67 LinkValue {
68 previous_value: String,
70 new_value: String,
72 },
73
74 ChangePinned {
75 previous_value: bool,
77
78 new_value: bool,
80 },
81}
82
83#[derive(Debug, Serialize, FromRow, ToSchema)]
84pub struct EditHistory {
85 #[schema(value_type = Uuid)]
87 pub id: EditHistoryId,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 #[schema(value_type = Option<Uuid>)]
92 pub file_id: Option<FileId>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 #[schema(value_type = Option<Uuid>)]
96 pub link_id: Option<LinkId>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 #[schema(value_type = Option<Uuid>)]
100 pub folder_id: Option<FolderId>,
101
102 pub user: Option<User>,
104
105 #[serde(rename = "type")]
107 #[sqlx(rename = "type")]
108 #[sqlx(try_from = "String")]
109 pub ty: EditHistoryType,
110
111 #[schema(value_type = EditHistoryMetadata)]
113 pub metadata: sqlx::types::Json<EditHistoryMetadata>,
114
115 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}