1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::{
4 postgres::{PgQueryResult, PgRow},
5 prelude::FromRow,
6};
7use tokio::try_join;
8use utoipa::ToSchema;
9use uuid::Uuid;
10
11use super::{
12 document_box::DocumentBoxScopeRaw,
13 file::{File, FileWithExtra},
14 link::{Link, LinkWithExtra},
15 user::{User, UserId},
16};
17use crate::{DbExecutor, DbPool, DbResult, models::shared::CountResult};
18
19pub type FolderId = Uuid;
20
21#[derive(Debug, Default, Serialize)]
23pub struct ResolvedFolder {
24 pub folders: Vec<Folder>,
26 pub files: Vec<File>,
28 pub links: Vec<Link>,
30}
31
32impl ResolvedFolder {
33 pub async fn resolve(db: &DbPool, folder: &Folder) -> DbResult<ResolvedFolder> {
34 let files_futures = File::find_by_parent(db, folder.id);
35 let folders_future = Folder::find_by_parent(db, folder.id);
36 let links_future = Link::find_by_parent(db, folder.id);
37
38 let (files, folders, links) = try_join!(files_futures, folders_future, links_future)?;
39
40 Ok(ResolvedFolder {
41 folders,
42 files,
43 links,
44 })
45 }
46}
47
48#[derive(Debug, Default, Serialize, ToSchema)]
51pub struct ResolvedFolderWithExtra {
52 pub path: Vec<FolderPathSegment>,
54 pub folders: Vec<FolderWithExtra>,
56 pub files: Vec<FileWithExtra>,
58 pub links: Vec<LinkWithExtra>,
60}
61
62impl ResolvedFolderWithExtra {
63 pub async fn resolve(db: &DbPool, folder_id: FolderId) -> DbResult<ResolvedFolderWithExtra> {
64 let path_future = Folder::resolve_path(db, folder_id);
65 let files_futures = File::find_by_parent_folder_with_extra(db, folder_id);
66 let folders_future = Folder::find_by_parent_with_extra(db, folder_id);
67 let links_future = Link::find_by_parent_with_extra(db, folder_id);
68
69 let (path, files, folders, links) =
70 try_join!(path_future, files_futures, folders_future, links_future)?;
71
72 Ok(ResolvedFolderWithExtra {
73 path,
74 folders,
75 files,
76 links,
77 })
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
82pub struct FolderPathSegment {
83 #[schema(value_type = Uuid)]
84 pub id: FolderId,
85 pub name: String,
86}
87
88impl<'r> FromRow<'r, PgRow> for FolderPathSegment {
89 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
90 use sqlx::Row;
91
92 let id = row.try_get(0)?;
93 let name = row.try_get(1)?;
94
95 Ok(FolderPathSegment { id, name })
96 }
97}
98
99#[derive(Debug, Clone, FromRow, Serialize)]
100pub struct Folder {
101 pub id: FolderId,
103 pub name: String,
105
106 pub pinned: bool,
108
109 pub document_box: DocumentBoxScopeRaw,
111 pub folder_id: Option<FolderId>,
113
114 pub created_at: DateTime<Utc>,
116
117 pub created_by: Option<UserId>,
119}
120
121#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
122pub struct FolderWithExtra {
123 #[schema(value_type = Uuid)]
125 pub id: FolderId,
126 pub name: String,
128
129 pub pinned: bool,
131
132 #[schema(value_type = Option<Uuid>)]
134 pub folder_id: Option<FolderId>,
135
136 pub created_at: DateTime<Utc>,
138 #[sqlx(flatten)]
140 #[schema(nullable, value_type = User)]
141 pub created_by: CreatedByUser,
142 pub last_modified_at: Option<DateTime<Utc>>,
144 #[sqlx(flatten)]
146 #[schema(nullable, value_type = User)]
147 pub last_modified_by: LastModifiedByUser,
148}
149
150#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
151pub struct WithFullPath<T> {
152 #[serde(flatten)]
153 #[sqlx(flatten)]
154 pub data: T,
155 #[sqlx(json)]
156 pub full_path: Vec<FolderPathSegment>,
157}
158
159#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
160pub struct WithFullPathScope<T> {
161 #[serde(flatten)]
162 #[sqlx(flatten)]
163 pub data: T,
164 #[sqlx(json)]
165 pub full_path: Vec<FolderPathSegment>,
166 pub document_box: DocumentBoxScopeRaw,
167}
168
169#[derive(Debug, Clone, Serialize)]
172#[serde(transparent)]
173pub struct CreatedByUser(pub Option<User>);
174
175impl<'r> FromRow<'r, PgRow> for CreatedByUser {
176 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
177 use sqlx::Row;
178
179 let id: Option<UserId> = row.try_get("cb_id")?;
180 if let Some(id) = id {
181 let name: Option<String> = row.try_get("cb_name")?;
182 let image_id: Option<String> = row.try_get("cb_image_id")?;
183 return Ok(CreatedByUser(Some(User { id, name, image_id })));
184 }
185
186 Ok(CreatedByUser(None))
187 }
188}
189
190#[derive(Debug, Clone, Serialize)]
193#[serde(transparent)]
194pub struct LastModifiedByUser(pub Option<User>);
195
196impl<'r> FromRow<'r, PgRow> for LastModifiedByUser {
197 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
198 use sqlx::Row;
199
200 let id: Option<UserId> = row.try_get("lmb_id")?;
201 if let Some(id) = id {
202 let name: Option<String> = row.try_get("lmb_name")?;
203 let image_id: Option<String> = row.try_get("lmb_image_id")?;
204 return Ok(LastModifiedByUser(Some(User { id, name, image_id })));
205 }
206
207 Ok(LastModifiedByUser(None))
208 }
209}
210
211pub struct CreateFolder {
212 pub name: String,
213 pub document_box: DocumentBoxScopeRaw,
214 pub folder_id: Option<FolderId>,
215 pub created_by: Option<UserId>,
216}
217
218#[derive(Debug, Serialize)]
219pub struct FolderChildrenCount {
220 pub file_count: i64,
221 pub link_count: i64,
222 pub folder_count: i64,
223}
224
225impl Folder {
226 pub async fn tree_all_children(&self, db: impl DbExecutor<'_>) -> DbResult<Vec<FolderId>> {
231 #[derive(FromRow)]
232 struct TempIdRow {
233 id: FolderId,
234 }
235
236 let results: Vec<TempIdRow> = sqlx::query_as(
237 r#"
238 -- Recursively collect all child folders
239 WITH RECURSIVE "folder_hierarchy" AS (
240 SELECT "id", "folder_id"
241 FROM "docbox_folders"
242 WHERE "docbox_folders"."id" = $1
243 UNION ALL (
244 SELECT
245 "folder"."id",
246 "folder"."folder_id"
247 FROM "docbox_folders" AS "folder"
248 INNER JOIN "folder_hierarchy" ON "folder"."folder_id" = "folder_hierarchy"."id"
249 )
250 )
251 CYCLE "id" SET "looped" USING "traversal_path"
252 SELECT "folder_hierarchy"."id" FROM "folder_hierarchy"
253 "#,
254 )
255 .bind(self.id)
256 .fetch_all(db)
257 .await?;
258
259 Ok(results.into_iter().map(|value| value.id).collect())
260 }
261
262 pub async fn count_children(
265 db: impl DbExecutor<'_>,
266 folder_id: FolderId,
267 ) -> DbResult<FolderChildrenCount> {
268 let (file_count, link_count, folder_count): (i64, i64, i64) = sqlx::query_as(
269 r#"
270 -- Recursively collect all child folders
271 WITH RECURSIVE "folder_hierarchy" AS (
272 SELECT "id", "folder_id"
273 FROM "docbox_folders"
274 WHERE "docbox_folders"."id" = $1
275 UNION ALL (
276 SELECT
277 "folder"."id",
278 "folder"."folder_id"
279 FROM "docbox_folders" AS "folder"
280 INNER JOIN "folder_hierarchy" ON "folder"."folder_id" = "folder_hierarchy"."id"
281 )
282 )
283 CYCLE "id" SET "looped" USING "traversal_path"
284 SELECT * FROM (
285 SELECT
286 -- Get counts of child tables
287 COUNT(DISTINCT "file"."id") AS "file_count",
288 COUNT(DISTINCT "link"."id") AS "link_count",
289 COUNT(DISTINCT "folder"."id") AS "folder_count"
290 FROM "folder_hierarchy"
291 -- Join on collections of files, links and folders
292 LEFT JOIN "docbox_files" AS "file" ON "file"."folder_id" = "folder_hierarchy"."id"
293 LEFT JOIN "docbox_links" AS "link" ON "link"."folder_id" = "folder_hierarchy"."id"
294 LEFT JOIN "docbox_folders" AS "folder" ON "folder"."folder_id" = "folder_hierarchy"."id"
295 ) AS "counts"
296 "#,
297 )
298 .bind(folder_id)
299 .fetch_one(db)
300 .await?;
301
302 Ok(FolderChildrenCount {
303 file_count,
304 link_count,
305 folder_count,
306 })
307 }
308
309 pub async fn resolve_path(
312 db: impl DbExecutor<'_>,
313 folder_id: FolderId,
314 ) -> DbResult<Vec<FolderPathSegment>> {
315 sqlx::query_as(
316 r#"
317 WITH RECURSIVE "folder_hierarchy" AS (
318 SELECT "id", "name", "folder_id", 0 AS "depth"
319 FROM "docbox_folders"
320 WHERE "docbox_folders"."id" = $1
321 UNION ALL (
322 SELECT
323 "folder"."id",
324 "folder"."name",
325 "folder"."folder_id",
326 "folder_hierarchy"."depth" + 1 as "depth"
327 FROM "docbox_folders" AS "folder"
328 INNER JOIN "folder_hierarchy" ON "folder"."id" = "folder_hierarchy"."folder_id"
329 )
330 )
331 CYCLE "id" SET "looped" USING "traversal_path"
332 SELECT "folder_hierarchy"."id", "folder_hierarchy"."name"
333 FROM "folder_hierarchy"
334 WHERE "folder_hierarchy"."id" <> $1
335 ORDER BY "folder_hierarchy"."depth" DESC
336 "#,
337 )
338 .bind(folder_id)
339 .fetch_all(db)
340 .await
341 }
342
343 pub async fn move_to_folder(
344 mut self,
345 db: impl DbExecutor<'_>,
346 folder_id: FolderId,
347 ) -> DbResult<Folder> {
348 debug_assert!(self.folder_id.is_some());
350
351 sqlx::query(r#"UPDATE "docbox_folders" SET "folder_id" = $1 WHERE "id" = $2"#)
352 .bind(folder_id)
353 .bind(self.id)
354 .execute(db)
355 .await?;
356
357 self.folder_id = Some(folder_id);
358
359 Ok(self)
360 }
361
362 pub async fn rename(mut self, db: impl DbExecutor<'_>, name: String) -> DbResult<Folder> {
363 sqlx::query(r#"UPDATE "docbox_folders" SET "name" = $1 WHERE "id" = $2"#)
364 .bind(name.as_str())
365 .bind(self.id)
366 .execute(db)
367 .await?;
368
369 self.name = name;
370
371 Ok(self)
372 }
373
374 pub async fn set_pinned(mut self, db: impl DbExecutor<'_>, pinned: bool) -> DbResult<Folder> {
375 sqlx::query(r#"UPDATE "docbox_folders" SET "pinned" = $1 WHERE "id" = $2"#)
376 .bind(pinned)
377 .bind(self.id)
378 .execute(db)
379 .await?;
380
381 self.pinned = pinned;
382
383 Ok(self)
384 }
385
386 pub async fn find_by_id(
387 db: impl DbExecutor<'_>,
388 scope: &DocumentBoxScopeRaw,
389 id: FolderId,
390 ) -> DbResult<Option<Folder>> {
391 sqlx::query_as(r#"SELECT * FROM "docbox_folders" WHERE "id" = $1 AND "document_box" = $2"#)
392 .bind(id)
393 .bind(scope)
394 .fetch_optional(db)
395 .await
396 }
397
398 pub async fn all_non_root(
401 db: impl DbExecutor<'_>,
402 offset: u64,
403 page_size: u64,
404 ) -> DbResult<Vec<Folder>> {
405 sqlx::query_as(
406 r#"
407 SELECT * FROM "docbox_folders"
408 WHERE "folder_id" IS NOT NULL
409 ORDER BY "created_at" ASC
410 OFFSET $1
411 LIMIT $2
412 "#,
413 )
414 .bind(offset as i64)
415 .bind(page_size as i64)
416 .fetch_all(db)
417 .await
418 }
419
420 pub async fn find_by_parent(
421 db: impl DbExecutor<'_>,
422 parent_id: FolderId,
423 ) -> DbResult<Vec<Folder>> {
424 sqlx::query_as(r#"SELECT * FROM "docbox_folders" WHERE "folder_id" = $1"#)
425 .bind(parent_id)
426 .fetch_all(db)
427 .await
428 }
429
430 pub async fn find_root(
431 db: impl DbExecutor<'_>,
432 document_box: &DocumentBoxScopeRaw,
433 ) -> DbResult<Option<Folder>> {
434 sqlx::query_as(
435 r#"SELECT * FROM "docbox_folders" WHERE "document_box" = $1 AND "folder_id" IS NULL"#,
436 )
437 .bind(document_box)
438 .fetch_optional(db)
439 .await
440 }
441
442 pub async fn create(
443 db: impl DbExecutor<'_>,
444 CreateFolder {
445 name,
446 document_box,
447 folder_id,
448 created_by,
449 }: CreateFolder,
450 ) -> DbResult<Folder> {
451 let folder = Folder {
452 id: Uuid::new_v4(),
453 name,
454 document_box,
455 folder_id,
456 created_by,
457 created_at: Utc::now(),
458 pinned: false,
459 };
460
461 sqlx::query(
462 r#"
463 INSERT INTO "docbox_folders" (
464 "id", "name", "document_box", "folder_id",
465 "created_by", "created_at"
466 )
467 VALUES ($1, $2, $3, $4, $5, $6)
468 "#,
469 )
470 .bind(folder.id)
471 .bind(folder.name.as_str())
472 .bind(folder.document_box.as_str())
473 .bind(folder.folder_id)
474 .bind(folder.created_by.as_ref())
475 .bind(folder.created_at)
476 .bind(folder.pinned)
477 .execute(db)
478 .await?;
479
480 Ok(folder)
481 }
482
483 pub async fn delete(&self, db: impl DbExecutor<'_>) -> DbResult<PgQueryResult> {
485 sqlx::query(r#"DELETE FROM "docbox_folders" WHERE "id" = $1"#)
486 .bind(self.id)
487 .execute(db)
488 .await
489 }
490
491 pub async fn resolve_with_extra_mixed_scopes(
494 db: impl DbExecutor<'_>,
495 folders_scope_with_id: Vec<(DocumentBoxScopeRaw, FolderId)>,
496 ) -> DbResult<Vec<WithFullPathScope<FolderWithExtra>>> {
497 if folders_scope_with_id.is_empty() {
498 return Ok(Vec::new());
499 }
500
501 let (scopes, folder_ids): (Vec<String>, Vec<FolderId>) =
502 folders_scope_with_id.into_iter().unzip();
503
504 sqlx::query_as(
505 r#"
506 -- Recursively resolve the folder paths for each folder creating a JSON array for the path
507 WITH RECURSIVE
508 "input_folders" AS (
509 SELECT folder_id, document_box
510 FROM UNNEST($1::text[], $2::uuid[]) AS t(document_box, folder_id)
511 ),
512 "folder_hierarchy" AS (
513 SELECT
514 "f"."id" AS "item_id",
515 "folder"."id" AS "folder_id",
516 "folder"."name" AS "folder_name",
517 "folder"."folder_id" AS "parent_folder_id",
518 0 AS "depth",
519 jsonb_build_array(jsonb_build_object('id', "folder"."id", 'name', "folder"."name")) AS "path"
520 FROM "docbox_folders" "f"
521 JOIN "input_folders" "i" ON "f"."id" = "i"."folder_id"
522 JOIN "docbox_folders" "folder" ON "f"."folder_id" = "folder"."id"
523 WHERE "folder"."document_box" = "i"."document_box"
524 UNION ALL
525 SELECT
526 "fh"."item_id",
527 "parent"."id",
528 "parent"."name",
529 "parent"."folder_id",
530 "fh"."depth" + 1,
531 jsonb_build_array(jsonb_build_object('id', "parent"."id", 'name', "parent"."name")) || "fh"."path"
532 FROM "folder_hierarchy" "fh"
533 JOIN "docbox_folders" "parent" ON "fh"."parent_folder_id" = "parent"."id"
534 ),
535 "folder_paths" AS (
536 SELECT "item_id", "path", ROW_NUMBER() OVER (PARTITION BY "item_id" ORDER BY "depth" DESC) AS "rn"
537 FROM "folder_hierarchy"
538 )
539 SELECT
540 -- folder itself
541 "folder".*,
542 -- Creator user details
543 "cu"."id" AS "cb_id",
544 "cu"."name" AS "cb_name",
545 "cu"."image_id" AS "cb_image_id",
546 -- Last modified date
547 "ehl"."created_at" AS "last_modified_at",
548 -- Last modified user details
549 "mu"."id" AS "lmb_id",
550 "mu"."name" AS "lmb_name",
551 "mu"."image_id" AS "lmb_image_id" ,
552 -- folder path from path lookup
553 "fp"."path" AS "full_path" ,
554 -- Include document box in response
555 "folder"."document_box" AS "document_box"
556 FROM "docbox_folders" AS "folder"
557 -- Join on the creator
558 LEFT JOIN "docbox_users" AS "cu"
559 ON "folder"."created_by" = "cu"."id"
560 -- Join on the edit history (Latest only)
561 LEFT JOIN (
562 -- Get the latest edit history entry
563 SELECT DISTINCT ON ("folder_id") "folder_id", "user_id", "created_at"
564 FROM "docbox_edit_history"
565 ORDER BY "folder_id", "created_at" DESC
566 ) AS "ehl" ON "folder"."id" = "ehl"."folder_id"
567 -- Join on the editor history latest edit user
568 LEFT JOIN "docbox_users" AS "mu" ON "ehl"."user_id" = "mu"."id"
569 -- Join on the resolved folder path
570 LEFT JOIN "folder_paths" "fp" ON "folder".id = "fp"."item_id" AND "fp".rn = 1
571 -- Join on the input files for filtering
572 JOIN "input_folders" "i" ON "folder"."id" = "i"."folder_id"
573 -- Ensure correct document box
574 WHERE "folder"."document_box" = "i"."document_box""#,
575 )
576 .bind(scopes)
577 .bind(folder_ids)
578 .fetch_all(db)
579 .await
580 }
581
582 pub async fn resolve_with_extra(
585 db: impl DbExecutor<'_>,
586 scope: &DocumentBoxScopeRaw,
587 folder_ids: Vec<Uuid>,
588 ) -> DbResult<Vec<WithFullPath<FolderWithExtra>>> {
589 if folder_ids.is_empty() {
590 return Ok(Vec::new());
591 }
592
593 sqlx::query_as(
594 r#"
595 -- Recursively resolve the folder paths for each folder creating a JSON array for the path
596 WITH RECURSIVE "folder_hierarchy" AS (
597 SELECT
598 "f"."id" AS "item_id",
599 "folder"."id" AS "folder_id",
600 "folder"."name" AS "folder_name",
601 "folder"."folder_id" AS "parent_folder_id",
602 0 AS "depth",
603 jsonb_build_array(jsonb_build_object('id', "folder"."id", 'name', "folder"."name")) AS "path"
604 FROM "docbox_folders" "f"
605 JOIN "docbox_folders" "folder" ON "f"."folder_id" = "folder"."id"
606 WHERE "f"."id" = ANY($1::uuid[]) AND "folder"."document_box" = $2
607 UNION ALL
608 SELECT
609 "fh"."item_id",
610 "parent"."id",
611 "parent"."name",
612 "parent"."folder_id",
613 "fh"."depth" + 1,
614 jsonb_build_array(jsonb_build_object('id', "parent"."id", 'name', "parent"."name")) || "fh"."path"
615 FROM "folder_hierarchy" "fh"
616 JOIN "docbox_folders" "parent" ON "fh"."parent_folder_id" = "parent"."id"
617 ),
618 "folder_paths" AS (
619 SELECT "item_id", "path", ROW_NUMBER() OVER (PARTITION BY "item_id" ORDER BY "depth" DESC) AS "rn"
620 FROM "folder_hierarchy"
621 )
622 SELECT
623 -- folder itself
624 "folder".*,
625 -- Creator user details
626 "cu"."id" AS "cb_id",
627 "cu"."name" AS "cb_name",
628 "cu"."image_id" AS "cb_image_id",
629 -- Last modified date
630 "ehl"."created_at" AS "last_modified_at",
631 -- Last modified user details
632 "mu"."id" AS "lmb_id",
633 "mu"."name" AS "lmb_name",
634 "mu"."image_id" AS "lmb_image_id" ,
635 -- folder path from path lookup
636 "fp"."path" AS "full_path"
637 FROM "docbox_folders" AS "folder"
638 -- Join on the creator
639 LEFT JOIN "docbox_users" AS "cu"
640 ON "folder"."created_by" = "cu"."id"
641 -- Join on the edit history (Latest only)
642 LEFT JOIN (
643 -- Get the latest edit history entry
644 SELECT DISTINCT ON ("folder_id") "folder_id", "user_id", "created_at"
645 FROM "docbox_edit_history"
646 ORDER BY "folder_id", "created_at" DESC
647 ) AS "ehl" ON "folder"."id" = "ehl"."folder_id"
648 -- Join on the editor history latest edit user
649 LEFT JOIN "docbox_users" AS "mu" ON "ehl"."user_id" = "mu"."id"
650 -- Join on the resolved folder path
651 LEFT JOIN "folder_paths" "fp" ON "folder".id = "fp"."item_id" AND "fp".rn = 1
652 WHERE "folder"."id" = ANY($1::uuid[]) AND "folder"."document_box" = $2"#,
653 )
654 .bind(folder_ids)
655 .bind(scope)
656 .fetch_all(db)
657 .await
658 }
659
660 pub async fn find_by_id_with_extra(
661 db: impl DbExecutor<'_>,
662 scope: &DocumentBoxScopeRaw,
663 id: FolderId,
664 ) -> DbResult<Option<FolderWithExtra>> {
665 sqlx::query_as(
666 r#"
667 SELECT
668 -- Folder itself
669 "folder".*,
670 -- Creator user details
671 "cu"."id" AS "cb_id",
672 "cu"."name" AS "cb_name",
673 "cu"."image_id" AS "cb_image_id",
674 -- Last modified date
675 "ehl"."created_at" AS "last_modified_at",
676 -- Last modified user details
677 "mu"."id" AS "lmb_id",
678 "mu"."name" AS "lmb_name",
679 "mu"."image_id" AS "lmb_image_id"
680 FROM "docbox_folders" AS "folder"
681 -- Join on the creator
682 LEFT JOIN "docbox_users" AS "cu"
683 ON "folder"."created_by" = "cu"."id"
684 -- Join on the edit history (Latest only)
685 LEFT JOIN (
686 -- Get the latest edit history entry
687 SELECT DISTINCT ON ("folder_id") "folder_id", "user_id", "created_at"
688 FROM "docbox_edit_history"
689 ORDER BY "folder_id", "created_at" DESC
690 ) AS "ehl" ON "folder"."id" = "ehl"."folder_id"
691 -- Join on the editor history latest edit user
692 LEFT JOIN "docbox_users" AS "mu" ON "ehl"."user_id" = "mu"."id"
693 WHERE "folder"."id" = $1 AND "folder"."document_box" = $2"#,
694 )
695 .bind(id)
696 .bind(scope)
697 .fetch_optional(db)
698 .await
699 }
700
701 pub async fn find_by_parent_with_extra(
702 db: impl DbExecutor<'_>,
703 parent_id: FolderId,
704 ) -> DbResult<Vec<FolderWithExtra>> {
705 sqlx::query_as(
706 r#"
707 SELECT
708 -- Folder itself
709 "folder".*,
710 -- Creator user details
711 "cu"."id" AS "cb_id",
712 "cu"."name" AS "cb_name",
713 "cu"."image_id" AS "cb_image_id",
714 -- Last modified date
715 "ehl"."created_at" AS "last_modified_at",
716 -- Last modified user details
717 "mu"."id" AS "lmb_id",
718 "mu"."name" AS "lmb_name",
719 "mu"."image_id" AS "lmb_image_id"
720 FROM "docbox_folders" AS "folder"
721 -- Join on the creator
722 LEFT JOIN "docbox_users" AS "cu"
723 ON "folder"."created_by" = "cu"."id"
724 -- Join on the edit history (Latest only)
725 LEFT JOIN (
726 -- Get the latest edit history entry
727 SELECT DISTINCT ON ("folder_id") "folder_id", "user_id", "created_at"
728 FROM "docbox_edit_history"
729 ORDER BY "folder_id", "created_at" DESC
730 ) AS "ehl" ON "folder"."id" = "ehl"."folder_id"
731 -- Join on the editor history latest edit user
732 LEFT JOIN "docbox_users" AS "mu" ON "ehl"."user_id" = "mu"."id"
733 WHERE "folder"."folder_id" = $1"#,
734 )
735 .bind(parent_id)
736 .fetch_all(db)
737 .await
738 }
739
740 pub async fn find_root_with_extra(
741 db: impl DbExecutor<'_>,
742 document_box: &DocumentBoxScopeRaw,
743 ) -> DbResult<Option<FolderWithExtra>> {
744 sqlx::query_as(
745 r#"
746 SELECT
747 -- Folder itself
748 "folder".*,
749 -- Creator user details
750 "cu"."id" AS "cb_id",
751 "cu"."name" AS "cb_name",
752 "cu"."image_id" AS "cb_image_id",
753 -- Last modified date
754 "ehl"."created_at" AS "last_modified_at",
755 -- Last modified user details
756 "mu"."id" AS "lmb_id",
757 "mu"."name" AS "lmb_name",
758 "mu"."image_id" AS "lmb_image_id"
759 FROM "docbox_folders" AS "folder"
760 -- Join on the creator
761 LEFT JOIN "docbox_users" AS "cu"
762 ON "folder"."created_by" = "cu"."id"
763 -- Join on the edit history (Latest only)
764 LEFT JOIN (
765 -- Get the latest edit history entry
766 SELECT DISTINCT ON ("folder_id") "folder_id", "user_id", "created_at"
767 FROM "docbox_edit_history"
768 ORDER BY "folder_id", "created_at" DESC
769 ) AS "ehl" ON "folder"."id" = "ehl"."folder_id"
770 -- Join on the editor history latest edit user
771 LEFT JOIN "docbox_users" AS "mu" ON "ehl"."user_id" = "mu"."id"
772 WHERE "folder"."document_box" = $1 AND "folder"."folder_id" IS NULL"#,
773 )
774 .bind(document_box)
775 .fetch_optional(db)
776 .await
777 }
778
779 pub async fn total_count(db: impl DbExecutor<'_>) -> DbResult<i64> {
781 let count_result: CountResult =
782 sqlx::query_as(r#"SELECT COUNT(*) AS "count" FROM "docbox_folders""#)
783 .fetch_one(db)
784 .await?;
785
786 Ok(count_result.count)
787 }
788}