1use serde::Serialize;
17use sqlx::PgPool;
18use uuid::Uuid;
19
20use crate::{
21 auth::Actor,
22 cadformat::{self, FormatKind},
23 error::{AppError, AppResult},
24 services::{access, Page},
25};
26
27pub const MAX_FOLDERS: i64 = 1000;
29
30#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
32pub struct SubFolder {
33 pub name: String,
34 #[sqlx(skip)]
36 pub path: String,
37 pub files: i64,
39}
40
41#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
44pub struct Entry {
45 pub file_id: Option<Uuid>,
46 pub filename: Option<String>,
47 pub size_bytes: Option<i64>,
48 pub format: String,
50 #[sqlx(skip)]
52 pub format_label: Option<&'static str>,
53 #[sqlx(skip)]
55 pub format_kind: Option<FormatKind>,
56 pub item_number: String,
57 pub item_type: String,
58 pub name: String,
60 pub code: String,
61 pub state: String,
62 pub checked_out_by_name: Option<String>,
63}
64
65#[derive(Clone, Debug, Serialize)]
67pub struct Level {
68 pub folder: String,
70 pub folders: Vec<SubFolder>,
71 pub entries: Vec<Entry>,
73 pub total: i64,
75}
76
77const LATEST: &str = "CROSS JOIN LATERAL (SELECT r.id, r.code, r.state, r.name, r.checked_out_by
79 FROM revisions r WHERE r.item_id = i.id
80 ORDER BY r.seq DESC LIMIT 1) lr";
81
82pub async fn level(
85 pool: &PgPool,
86 actor: &Actor,
87 project_id: Uuid,
88 folder: &str,
89 page: Page,
90) -> AppResult<Level> {
91 access::ensure_visible(
92 actor,
93 Some(project_id),
94 AppError::not_found("Project group not found."),
95 )?;
96 let visible = access::item_visible("i", 2);
97 let mut folders = sqlx::query_as::<_, SubFolder>(&format!(
101 "SELECT sub.name, COUNT(*) AS files
102 FROM (SELECT split_part(CASE WHEN $3 = '' THEN f.folder
103 ELSE substr(f.folder, length($3) + 2) END, '/', 1) AS name
104 FROM items i {LATEST}
105 JOIN files f ON f.revision_id = lr.id
106 WHERE i.project_id = $1 AND {visible}
107 AND CASE WHEN $3 = '' THEN f.folder <> ''
108 ELSE left(f.folder, length($3) + 1) = $3 || '/' END) sub
109 GROUP BY sub.name
110 ORDER BY lower(sub.name), sub.name
111 LIMIT {MAX_FOLDERS}"
112 ))
113 .bind(project_id)
114 .bind(&actor.hidden_projects)
115 .bind(folder)
116 .fetch_all(pool)
117 .await?;
118 for sub in &mut folders {
119 sub.path = if folder.is_empty() {
120 sub.name.clone()
121 } else {
122 format!("{folder}/{}", sub.name)
123 };
124 }
125
126 #[derive(sqlx::FromRow)]
127 struct Counted {
128 #[sqlx(flatten)]
129 entry: Entry,
130 total: i64,
131 }
132 let rows = sqlx::query_as::<_, Counted>(&format!(
133 "SELECT e.*, COUNT(*) OVER () AS total
134 FROM (SELECT f.id AS file_id, f.filename, f.size_bytes, COALESCE(f.format, '') AS format,
135 i.number AS item_number, i.item_type, lr.name, lr.code, lr.state,
136 u.display_name AS checked_out_by_name
137 FROM items i {LATEST}
138 JOIN files f ON f.revision_id = lr.id
139 LEFT JOIN users u ON u.id = lr.checked_out_by
140 WHERE i.project_id = $1 AND {visible} AND f.folder = $3
141 UNION ALL
142 SELECT NULL::uuid, NULL::text, NULL::bigint, '',
143 i.number, i.item_type, lr.name, lr.code, lr.state, u.display_name
144 FROM items i {LATEST}
145 LEFT JOIN users u ON u.id = lr.checked_out_by
146 WHERE i.project_id = $1 AND {visible} AND $3 = ''
147 AND NOT EXISTS (SELECT 1 FROM files f WHERE f.revision_id = lr.id)) e
148 ORDER BY (e.filename IS NULL), lower(COALESCE(e.filename, e.name)), e.filename,
149 e.item_number
150 LIMIT $4 OFFSET $5"
151 ))
152 .bind(project_id)
153 .bind(&actor.hidden_projects)
154 .bind(folder)
155 .bind(page.limit)
156 .bind(page.offset)
157 .fetch_all(pool)
158 .await?;
159 let total = rows.first().map(|r| r.total).unwrap_or(0);
160 let entries = rows
161 .into_iter()
162 .map(|r| {
163 let mut entry = r.entry;
164 if let Some(info) = cadformat::info(&entry.format) {
165 entry.format_label = Some(info.label);
166 entry.format_kind = Some(info.kind);
167 }
168 entry
169 })
170 .collect();
171 Ok(Level {
172 folder: folder.to_string(),
173 folders,
174 entries,
175 total,
176 })
177}