1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5use utoipa::{IntoParams, ToSchema};
6use uuid::Uuid;
7
8use super::Visibility;
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum LibraryIngestStatus {
13 Pending,
14 Running,
15 Succeeded,
16 Failed,
17}
18
19impl LibraryIngestStatus {
20 pub fn as_str(self) -> &'static str {
21 match self {
22 Self::Pending => "pending",
23 Self::Running => "running",
24 Self::Succeeded => "succeeded",
25 Self::Failed => "failed",
26 }
27 }
28}
29
30impl std::str::FromStr for LibraryIngestStatus {
31 type Err = anyhow::Error;
32
33 fn from_str(value: &str) -> Result<Self, Self::Err> {
34 match value {
35 "pending" => Ok(Self::Pending),
36 "running" => Ok(Self::Running),
37 "succeeded" => Ok(Self::Succeeded),
38 "failed" => Ok(Self::Failed),
39 other => Err(anyhow::anyhow!(
40 "unsupported library ingest status: {other}"
41 )),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
47pub struct CreateFolderRequest {
48 #[serde(default)]
49 pub parent_folder_id: Option<Uuid>,
50 pub name: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
54pub struct MoveFolderRequest {
55 #[serde(default)]
56 pub target_folder_id: Option<Uuid>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
60pub struct MoveFileRequest {
61 #[serde(default)]
62 pub target_folder_id: Option<Uuid>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
66pub struct CreateTextRequest {
67 #[serde(default)]
68 pub folder_id: Option<Uuid>,
69 pub title: String,
70 pub content: String,
71 #[serde(default = "default_text_content_format")]
72 pub content_format: LibraryTextContentFormat,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub source_uri: Option<String>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub summary: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
80pub struct UpsertLibraryTextRequest {
81 pub external_id: String,
82 #[serde(default)]
83 pub folder_id: Option<Uuid>,
84 pub title: String,
85 pub content: String,
86 #[serde(default = "default_text_content_format")]
87 pub content_format: LibraryTextContentFormat,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub source_uri: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub summary: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub published_at: Option<DateTime<Utc>>,
94 #[serde(default = "default_metadata_json")]
95 #[schema(value_type = Object)]
96 pub metadata_json: Value,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
100pub struct LibraryFileSummary {
101 pub file_id: Uuid,
102 pub group_key: String,
103 pub group_path: String,
104 pub visibility: Visibility,
105 #[serde(default)]
106 pub folder_id: Option<Uuid>,
107 pub filename: String,
108 pub media_type: String,
109 pub size_bytes: i64,
110 pub ingest_status: LibraryIngestStatus,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub error_message: Option<String>,
113 pub created_at: DateTime<Utc>,
114 pub updated_at: DateTime<Utc>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub ingested_at: Option<DateTime<Utc>>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
120pub struct LibraryFolderNode {
121 pub group_key: String,
122 pub group_path: String,
123 pub visibility: Visibility,
124 #[serde(default)]
125 pub folder_id: Option<Uuid>,
126 #[serde(default)]
127 pub parent_folder_id: Option<Uuid>,
128 pub name: String,
129 pub path: String,
130 pub processing_count: usize,
131 #[schema(no_recursion)]
132 pub children: Vec<LibraryFolderNode>,
133 pub files: Vec<LibraryFileSummary>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
137pub struct LibraryFolderResponse {
138 pub folder_id: Uuid,
139 pub group_key: String,
140 pub group_path: String,
141 pub visibility: Visibility,
142 #[serde(default)]
143 pub parent_folder_id: Option<Uuid>,
144 pub name: String,
145 pub path: String,
146 pub created_at: DateTime<Utc>,
147 pub updated_at: DateTime<Utc>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
151pub struct LibraryTreeResponse {
152 pub root: LibraryFolderNode,
153}
154
155#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
156#[serde(rename_all = "snake_case")]
157pub enum LibraryResourceKind {
158 Folder,
159 File,
160}
161
162#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
163#[serde(rename_all = "snake_case")]
164pub enum LibraryResourceSortBy {
165 Name,
166 Type,
167 Status,
168 Size,
169 UpdatedAt,
170}
171
172impl LibraryResourceSortBy {
173 pub fn as_str(self) -> &'static str {
174 match self {
175 Self::Name => "name",
176 Self::Type => "type",
177 Self::Status => "status",
178 Self::Size => "size",
179 Self::UpdatedAt => "updated_at",
180 }
181 }
182}
183
184#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
185#[serde(rename_all = "snake_case")]
186pub enum SortDirection {
187 Asc,
188 Desc,
189}
190
191impl SortDirection {
192 pub fn as_str(self) -> &'static str {
193 match self {
194 Self::Asc => "asc",
195 Self::Desc => "desc",
196 }
197 }
198}
199
200fn default_page() -> u32 {
201 1
202}
203
204fn default_page_size() -> u32 {
205 50
206}
207
208fn default_resource_sort_by() -> LibraryResourceSortBy {
209 LibraryResourceSortBy::UpdatedAt
210}
211
212fn default_sort_direction() -> SortDirection {
213 SortDirection::Desc
214}
215
216#[derive(Debug, Clone, Deserialize, IntoParams)]
217#[into_params(parameter_in = Query)]
218pub struct LibraryResourcePageQuery {
219 #[serde(default)]
220 pub folder_id: Option<Uuid>,
221 #[serde(default = "default_page")]
222 pub page: u32,
223 #[serde(default = "default_page_size")]
224 pub page_size: u32,
225 #[serde(default)]
226 pub query: Option<String>,
227 #[serde(default)]
228 pub status: Option<LibraryIngestStatus>,
229 #[serde(default = "default_resource_sort_by")]
230 pub sort_by: LibraryResourceSortBy,
231 #[serde(default = "default_sort_direction")]
232 pub sort_direction: SortDirection,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
236pub struct LibraryResourceItem {
237 pub kind: LibraryResourceKind,
238 pub id: Uuid,
239 pub group_key: String,
240 pub group_path: String,
241 pub visibility: Visibility,
242 #[serde(default)]
243 pub parent_folder_id: Option<Uuid>,
244 pub name: String,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub media_type: Option<String>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub size_bytes: Option<i64>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub ingest_status: Option<LibraryIngestStatus>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub error_message: Option<String>,
253 pub child_folder_count: u64,
254 pub file_count: u64,
255 pub processing_count: u64,
256 pub is_source_folder: bool,
257 pub is_source_records_folder: bool,
258 pub created_at: DateTime<Utc>,
259 pub updated_at: DateTime<Utc>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
263pub struct LibraryResourcePageResponse {
264 pub items: Vec<LibraryResourceItem>,
265 pub page: u32,
266 pub page_size: u32,
267 pub total: u64,
268 pub total_pages: u32,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
272pub struct LibraryIngestJobResponse {
273 pub job_id: Uuid,
274 pub group_key: String,
275 pub group_path: String,
276 pub visibility: Visibility,
277 pub file_id: Uuid,
278 pub status: LibraryIngestStatus,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub docling_task_id: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub error_message: Option<String>,
283 pub created_at: DateTime<Utc>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub started_at: Option<DateTime<Utc>>,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub finished_at: Option<DateTime<Utc>>,
288 pub updated_at: DateTime<Utc>,
289}
290
291#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
292#[serde(rename_all = "snake_case")]
293pub enum LibraryTextContentFormat {
294 PlainText,
295 Markdown,
296}
297
298#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
299#[serde(rename_all = "snake_case")]
300pub enum LibraryPreviewContentFormat {
301 PlainText,
302 Markdown,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
306pub struct LibraryDocumentSectionPreview {
307 pub document_id: i64,
308 pub section_key: String,
309 pub section_label: String,
310 pub sort_order: i32,
311 pub title: String,
312 pub preview_text: String,
313 #[serde(default = "default_preview_content_format")]
314 pub content_format: LibraryPreviewContentFormat,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
318pub struct LibraryFileDetailResponse {
319 pub file_id: Uuid,
320 pub group_key: String,
321 pub group_path: String,
322 pub visibility: Visibility,
323 #[serde(default)]
324 pub folder_id: Option<Uuid>,
325 pub folder_path: String,
326 pub filename: String,
327 pub media_type: String,
328 pub size_bytes: i64,
329 pub sha256: String,
330 #[serde(default)]
331 pub source_available: bool,
332 pub ingest_status: LibraryIngestStatus,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub error_message: Option<String>,
335 pub created_at: DateTime<Utc>,
336 pub updated_at: DateTime<Utc>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub ingested_at: Option<DateTime<Utc>>,
339 pub sections: Vec<LibraryDocumentSectionPreview>,
340 pub jobs: Vec<LibraryIngestJobResponse>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
344pub struct LibraryUploadResponse {
345 pub files: Vec<LibraryFileSummary>,
346 pub jobs: Vec<LibraryIngestJobResponse>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
350pub struct PrepareLibraryUploadRequest {
351 #[serde(default)]
352 pub folder_id: Option<Uuid>,
353 pub filename: String,
354 pub media_type: String,
355 pub size_bytes: i64,
356 pub sha256: String,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
360pub struct PrepareLibraryUploadResponse {
361 pub upload_required: bool,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub file: Option<LibraryFileSummary>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub job: Option<LibraryIngestJobResponse>,
366}
367
368fn default_preview_content_format() -> LibraryPreviewContentFormat {
369 LibraryPreviewContentFormat::PlainText
370}
371
372fn default_text_content_format() -> LibraryTextContentFormat {
373 LibraryTextContentFormat::PlainText
374}
375
376fn default_metadata_json() -> Value {
377 json!({})
378}