1use super::{
2 document_box::DocumentBoxScopeRaw,
3 file::{File, FileWithExtra},
4 link::{Link, LinkWithExtra},
5 user::{User, UserId},
6};
7use crate::{
8 DbExecutor, DbPool, DbResult,
9 models::shared::{CountResult, DocboxInputPair, FolderPathSegment, WithFullPath},
10};
11use chrono::{DateTime, Utc};
12use serde::Serialize;
13use sqlx::{postgres::PgQueryResult, prelude::FromRow};
14use tokio::try_join;
15use utoipa::ToSchema;
16use uuid::Uuid;
17
18pub type FolderId = Uuid;
19
20#[derive(Debug, Default, Serialize)]
22pub struct ResolvedFolder {
23 pub folders: Vec<Folder>,
25 pub files: Vec<File>,
27 pub links: Vec<Link>,
29}
30
31impl ResolvedFolder {
32 pub async fn resolve(db: &DbPool, folder_id: FolderId) -> DbResult<ResolvedFolder> {
33 let files_futures = File::find_by_parent(db, folder_id);
34 let folders_future = Folder::find_by_parent(db, folder_id);
35 let links_future = Link::find_by_parent(db, folder_id);
36
37 let (files, folders, links) = try_join!(files_futures, folders_future, links_future)?;
38
39 Ok(ResolvedFolder {
40 folders,
41 files,
42 links,
43 })
44 }
45}
46
47#[derive(Debug, Default, Serialize, ToSchema)]
50pub struct ResolvedFolderWithExtra {
51 pub path: Vec<FolderPathSegment>,
53 pub folders: Vec<FolderWithExtra>,
55 pub files: Vec<FileWithExtra>,
57 pub links: Vec<LinkWithExtra>,
59}
60
61impl ResolvedFolderWithExtra {
62 pub async fn resolve(
63 db: &DbPool,
64 folder_id: FolderId,
65 path: Vec<FolderPathSegment>,
66 ) -> DbResult<ResolvedFolderWithExtra> {
67 let files_futures = File::find_by_parent_folder_with_extra(db, folder_id);
68 let folders_future = Folder::find_by_parent_with_extra(db, folder_id);
69 let links_future = Link::find_by_parent_with_extra(db, folder_id);
70
71 let (files, folders, links) = try_join!(files_futures, folders_future, links_future)?;
72
73 Ok(ResolvedFolderWithExtra {
74 path,
75 folders,
76 files,
77 links,
78 })
79 }
80}
81
82#[derive(Debug, Clone, Serialize, ToSchema, FromRow, sqlx::Type)]
83#[sqlx(type_name = "docbox_folder")]
84pub struct Folder {
85 #[schema(value_type = Uuid)]
87 pub id: FolderId,
88 pub name: String,
90
91 pub pinned: bool,
93
94 pub document_box: DocumentBoxScopeRaw,
96 #[schema(value_type = Option<Uuid>)]
98 pub folder_id: Option<FolderId>,
99
100 pub created_at: DateTime<Utc>,
102 #[serde(skip)]
104 pub created_by: Option<UserId>,
105}
106
107impl Eq for Folder {}
108
109impl PartialEq for Folder {
110 fn eq(&self, other: &Self) -> bool {
111 self.id.eq(&other.id)
112 && self.name.eq(&other.name)
113 && self.pinned.eq(&other.pinned)
114 && self.document_box.eq(&other.document_box)
115 && self.folder_id.eq(&other.folder_id)
116 && self.created_by.eq(&self.created_by)
117 && self
120 .created_at
121 .timestamp_millis()
122 .eq(&other.created_at.timestamp_millis())
123 }
124}
125
126#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
127pub struct FolderWithExtra {
128 #[serde(flatten)]
129 pub folder: Folder,
130 #[schema(nullable, value_type = User)]
131 pub created_by: Option<User>,
132 #[schema(nullable, value_type = User)]
133 pub last_modified_by: Option<User>,
134 pub last_modified_at: Option<DateTime<Utc>>,
136}
137
138#[derive(Debug, Clone, Default)]
139pub struct CreateFolder {
140 pub name: String,
141 pub document_box: DocumentBoxScopeRaw,
142 pub folder_id: Option<FolderId>,
143 pub created_by: Option<UserId>,
144}
145
146#[derive(Debug, Serialize)]
147pub struct FolderChildrenCount {
148 pub file_count: i64,
149 pub link_count: i64,
150 pub folder_count: i64,
151}
152
153impl Folder {
154 pub async fn create(
155 db: impl DbExecutor<'_>,
156 CreateFolder {
157 name,
158 document_box,
159 folder_id,
160 created_by,
161 }: CreateFolder,
162 ) -> DbResult<Folder> {
163 let folder = Folder {
164 id: Uuid::new_v4(),
165 name,
166 document_box,
167 folder_id,
168 created_by,
169 created_at: Utc::now(),
170 pinned: false,
171 };
172
173 sqlx::query(
174 r#"
175 INSERT INTO "docbox_folders" (
176 "id", "name", "document_box", "folder_id",
177 "created_by", "created_at"
178 )
179 VALUES ($1, $2, $3, $4, $5, $6)
180 "#,
181 )
182 .bind(folder.id)
183 .bind(folder.name.as_str())
184 .bind(folder.document_box.as_str())
185 .bind(folder.folder_id)
186 .bind(folder.created_by.as_ref())
187 .bind(folder.created_at)
188 .bind(folder.pinned)
189 .execute(db)
190 .await?;
191
192 Ok(folder)
193 }
194
195 pub async fn tree_all_children(&self, db: impl DbExecutor<'_>) -> DbResult<Vec<FolderId>> {
200 #[derive(FromRow)]
201 struct TempIdRow {
202 id: FolderId,
203 }
204
205 let results: Vec<TempIdRow> =
206 sqlx::query_as(r#"SELECT "id" FROM recursive_folder_children_ids($1)"#)
207 .bind(self.id)
208 .fetch_all(db)
209 .await?;
210
211 Ok(results.into_iter().map(|value| value.id).collect())
212 }
213
214 pub async fn count_children(
217 db: impl DbExecutor<'_>,
218 folder_id: FolderId,
219 ) -> DbResult<FolderChildrenCount> {
220 let (file_count, link_count, folder_count): (i64, i64, i64) =
221 sqlx::query_as(r#"SELECT * FROM count_folder_children($1) AS "counts""#)
222 .bind(folder_id)
223 .fetch_one(db)
224 .await?;
225
226 Ok(FolderChildrenCount {
227 file_count,
228 link_count,
229 folder_count,
230 })
231 }
232
233 pub async fn resolve_path(
236 db: impl DbExecutor<'_>,
237 folder_id: FolderId,
238 ) -> DbResult<Vec<FolderPathSegment>> {
239 sqlx::query_as(r#"SELECT "id", "name" FROM resolve_folder_path($1)"#)
240 .bind(folder_id)
241 .fetch_all(db)
242 .await
243 }
244
245 pub async fn move_to_folder(
246 mut self,
247 db: impl DbExecutor<'_>,
248 folder_id: FolderId,
249 ) -> DbResult<Folder> {
250 debug_assert!(self.folder_id.is_some());
252
253 sqlx::query(r#"UPDATE "docbox_folders" SET "folder_id" = $1 WHERE "id" = $2"#)
254 .bind(folder_id)
255 .bind(self.id)
256 .execute(db)
257 .await?;
258
259 self.folder_id = Some(folder_id);
260
261 Ok(self)
262 }
263
264 pub async fn rename(mut self, db: impl DbExecutor<'_>, name: String) -> DbResult<Folder> {
265 sqlx::query(r#"UPDATE "docbox_folders" SET "name" = $1 WHERE "id" = $2"#)
266 .bind(name.as_str())
267 .bind(self.id)
268 .execute(db)
269 .await?;
270
271 self.name = name;
272
273 Ok(self)
274 }
275
276 pub async fn set_pinned(mut self, db: impl DbExecutor<'_>, pinned: bool) -> DbResult<Folder> {
277 sqlx::query(r#"UPDATE "docbox_folders" SET "pinned" = $1 WHERE "id" = $2"#)
278 .bind(pinned)
279 .bind(self.id)
280 .execute(db)
281 .await?;
282
283 self.pinned = pinned;
284
285 Ok(self)
286 }
287
288 pub async fn find_by_id(
289 db: impl DbExecutor<'_>,
290 scope: &DocumentBoxScopeRaw,
291 id: FolderId,
292 ) -> DbResult<Option<Folder>> {
293 sqlx::query_as(r#"SELECT * FROM "docbox_folders" WHERE "id" = $1 AND "document_box" = $2"#)
294 .bind(id)
295 .bind(scope)
296 .fetch_optional(db)
297 .await
298 }
299
300 pub async fn all_non_root(
303 db: impl DbExecutor<'_>,
304 offset: u64,
305 page_size: u64,
306 ) -> DbResult<Vec<Folder>> {
307 sqlx::query_as(
308 r#"
309 SELECT * FROM "docbox_folders"
310 WHERE "folder_id" IS NOT NULL
311 ORDER BY "created_at" ASC
312 OFFSET $1
313 LIMIT $2
314 "#,
315 )
316 .bind(offset as i64)
317 .bind(page_size as i64)
318 .fetch_all(db)
319 .await
320 }
321
322 pub async fn find_by_parent(
323 db: impl DbExecutor<'_>,
324 parent_id: FolderId,
325 ) -> DbResult<Vec<Folder>> {
326 sqlx::query_as(r#"SELECT * FROM "docbox_folders" WHERE "folder_id" = $1"#)
327 .bind(parent_id)
328 .fetch_all(db)
329 .await
330 }
331
332 pub async fn find_root(
333 db: impl DbExecutor<'_>,
334 document_box: &DocumentBoxScopeRaw,
335 ) -> DbResult<Option<Folder>> {
336 sqlx::query_as(
337 r#"SELECT * FROM "docbox_folders" WHERE "document_box" = $1 AND "folder_id" IS NULL"#,
338 )
339 .bind(document_box)
340 .fetch_optional(db)
341 .await
342 }
343
344 pub async fn delete(&self, db: impl DbExecutor<'_>) -> DbResult<PgQueryResult> {
346 sqlx::query(r#"DELETE FROM "docbox_folders" WHERE "id" = $1"#)
347 .bind(self.id)
348 .execute(db)
349 .await
350 }
351
352 pub async fn resolve_with_extra_mixed_scopes(
355 db: impl DbExecutor<'_>,
356 folders_scope_with_id: Vec<DocboxInputPair<'_>>,
357 ) -> DbResult<Vec<WithFullPath<FolderWithExtra>>> {
358 if folders_scope_with_id.is_empty() {
359 return Ok(Vec::new());
360 }
361
362 sqlx::query_as(r#"SELECT * FROM resolve_folders_with_extra_mixed_scopes($1)"#)
363 .bind(folders_scope_with_id)
364 .fetch_all(db)
365 .await
366 }
367
368 pub async fn resolve_with_extra(
371 db: impl DbExecutor<'_>,
372 scope: &DocumentBoxScopeRaw,
373 folder_ids: Vec<Uuid>,
374 ) -> DbResult<Vec<WithFullPath<FolderWithExtra>>> {
375 if folder_ids.is_empty() {
376 return Ok(Vec::new());
377 }
378
379 sqlx::query_as(r#"SELECT * FROM resolve_folders_with_extra($1, $2)"#)
380 .bind(scope)
381 .bind(folder_ids)
382 .fetch_all(db)
383 .await
384 }
385
386 pub async fn find_by_id_with_extra(
387 db: impl DbExecutor<'_>,
388 scope: &DocumentBoxScopeRaw,
389 id: FolderId,
390 ) -> DbResult<Option<WithFullPath<FolderWithExtra>>> {
391 sqlx::query_as(r#"SELECT * FROM resolve_folder_by_id_with_extra($1, $2)"#)
392 .bind(scope)
393 .bind(id)
394 .fetch_optional(db)
395 .await
396 }
397
398 pub async fn find_by_parent_with_extra(
399 db: impl DbExecutor<'_>,
400 parent_id: FolderId,
401 ) -> DbResult<Vec<FolderWithExtra>> {
402 sqlx::query_as(r#"SELECT * FROM resolve_folder_by_parent_with_extra($1)"#)
403 .bind(parent_id)
404 .fetch_all(db)
405 .await
406 }
407
408 pub async fn find_root_with_extra(
409 db: impl DbExecutor<'_>,
410 document_box: &DocumentBoxScopeRaw,
411 ) -> DbResult<Option<WithFullPath<FolderWithExtra>>> {
412 sqlx::query_as(r#"SELECT * FROM resolve_root_folder_with_extra($1)"#)
413 .bind(document_box)
414 .fetch_optional(db)
415 .await
416 }
417
418 pub async fn total_count(db: impl DbExecutor<'_>) -> DbResult<i64> {
420 let count_result: CountResult =
421 sqlx::query_as(r#"SELECT COUNT(*) AS "count" FROM "docbox_folders""#)
422 .fetch_one(db)
423 .await?;
424
425 Ok(count_result.count)
426 }
427}