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 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub translation: Option<crate::TranslationDirective>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
82pub struct UpsertLibraryTextRequest {
83 pub external_id: String,
84 #[serde(default)]
85 pub folder_id: Option<Uuid>,
86 pub title: String,
87 pub content: String,
88 #[serde(default = "default_text_content_format")]
89 pub content_format: LibraryTextContentFormat,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub source_uri: Option<String>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub summary: Option<String>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub published_at: Option<DateTime<Utc>>,
96 #[serde(default = "default_metadata_json")]
97 #[schema(value_type = Object)]
98 pub metadata_json: Value,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub translation: Option<crate::TranslationDirective>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
104pub struct LibraryFileSummary {
105 pub file_id: Uuid,
106 pub group_key: String,
107 pub group_path: String,
108 pub visibility: Visibility,
109 #[serde(default)]
110 pub folder_id: Option<Uuid>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub external_id: Option<String>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub source_uri: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub published_at: Option<DateTime<Utc>>,
117 #[serde(default = "default_metadata_json")]
118 #[schema(value_type = Object)]
119 pub metadata_json: Value,
120 pub filename: String,
121 pub media_type: String,
122 pub size_bytes: i64,
123 pub ingest_status: LibraryIngestStatus,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub error_message: Option<String>,
126 pub created_at: DateTime<Utc>,
127 pub updated_at: DateTime<Utc>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub ingested_at: Option<DateTime<Utc>>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
133pub struct LibraryFolderNode {
134 pub group_key: String,
135 pub group_path: String,
136 pub visibility: Visibility,
137 #[serde(default)]
138 pub folder_id: Option<Uuid>,
139 #[serde(default)]
140 pub parent_folder_id: Option<Uuid>,
141 pub name: String,
142 pub path: String,
143 pub processing_count: usize,
144 #[schema(no_recursion)]
145 pub children: Vec<LibraryFolderNode>,
146 pub files: Vec<LibraryFileSummary>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
150pub struct LibraryFolderResponse {
151 pub folder_id: Uuid,
152 pub group_key: String,
153 pub group_path: String,
154 pub visibility: Visibility,
155 #[serde(default)]
156 pub parent_folder_id: Option<Uuid>,
157 pub name: String,
158 pub path: String,
159 pub created_at: DateTime<Utc>,
160 pub updated_at: DateTime<Utc>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
164pub struct LibraryTreeResponse {
165 pub root: LibraryFolderNode,
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
169#[serde(rename_all = "snake_case")]
170pub enum LibraryResourceKind {
171 Folder,
172 File,
173}
174
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
176#[serde(rename_all = "snake_case")]
177pub enum LibraryResourceSortBy {
178 Name,
179 Type,
180 Status,
181 Size,
182 UpdatedAt,
183}
184
185impl LibraryResourceSortBy {
186 pub fn as_str(self) -> &'static str {
187 match self {
188 Self::Name => "name",
189 Self::Type => "type",
190 Self::Status => "status",
191 Self::Size => "size",
192 Self::UpdatedAt => "updated_at",
193 }
194 }
195}
196
197#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
198#[serde(rename_all = "snake_case")]
199pub enum SortDirection {
200 Asc,
201 Desc,
202}
203
204impl SortDirection {
205 pub fn as_str(self) -> &'static str {
206 match self {
207 Self::Asc => "asc",
208 Self::Desc => "desc",
209 }
210 }
211}
212
213fn default_page() -> u32 {
214 1
215}
216
217fn default_page_size() -> u32 {
218 50
219}
220
221fn default_resource_sort_by() -> LibraryResourceSortBy {
222 LibraryResourceSortBy::UpdatedAt
223}
224
225fn default_sort_direction() -> SortDirection {
226 SortDirection::Desc
227}
228
229#[derive(Debug, Clone, Deserialize, IntoParams)]
230#[into_params(parameter_in = Query)]
231pub struct LibraryResourcePageQuery {
232 #[serde(default)]
233 pub folder_id: Option<Uuid>,
234 #[serde(default = "default_page")]
235 pub page: u32,
236 #[serde(default = "default_page_size")]
237 pub page_size: u32,
238 #[serde(default)]
239 pub query: Option<String>,
240 #[serde(default)]
241 pub status: Option<LibraryIngestStatus>,
242 #[serde(default = "default_resource_sort_by")]
243 pub sort_by: LibraryResourceSortBy,
244 #[serde(default = "default_sort_direction")]
245 pub sort_direction: SortDirection,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
249pub struct LibraryResourceItem {
250 pub kind: LibraryResourceKind,
251 pub id: Uuid,
252 pub group_key: String,
253 pub group_path: String,
254 pub visibility: Visibility,
255 #[serde(default)]
256 pub parent_folder_id: Option<Uuid>,
257 pub name: String,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub media_type: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub size_bytes: Option<i64>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub ingest_status: Option<LibraryIngestStatus>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub error_message: Option<String>,
266 pub child_folder_count: u64,
267 pub file_count: u64,
268 pub processing_count: u64,
269 pub is_source_folder: bool,
270 pub is_source_records_folder: bool,
271 pub created_at: DateTime<Utc>,
272 pub updated_at: DateTime<Utc>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
276pub struct LibraryResourcePageResponse {
277 pub items: Vec<LibraryResourceItem>,
278 pub page: u32,
279 pub page_size: u32,
280 pub total: u64,
281 pub total_pages: u32,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
285pub struct LibraryIngestJobResponse {
286 pub job_id: Uuid,
287 pub group_key: String,
288 pub group_path: String,
289 pub visibility: Visibility,
290 pub file_id: Uuid,
291 pub status: LibraryIngestStatus,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 pub docling_task_id: Option<String>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub error_message: Option<String>,
296 pub created_at: DateTime<Utc>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub started_at: Option<DateTime<Utc>>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub finished_at: Option<DateTime<Utc>>,
301 pub updated_at: DateTime<Utc>,
302}
303
304#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
305#[serde(rename_all = "snake_case")]
306pub enum LibraryTextContentFormat {
307 PlainText,
308 Markdown,
309}
310
311#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
312#[serde(rename_all = "snake_case")]
313pub enum LibraryPreviewContentFormat {
314 PlainText,
315 Markdown,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
319pub struct LibraryDocumentSectionPreview {
320 pub document_id: i64,
321 pub section_key: String,
322 pub section_label: String,
323 pub sort_order: i32,
324 pub title: String,
325 pub preview_text: String,
326 #[serde(default = "default_preview_content_format")]
327 pub content_format: LibraryPreviewContentFormat,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
331pub struct LibraryFileDetailResponse {
332 pub file_id: Uuid,
333 pub group_key: String,
334 pub group_path: String,
335 pub visibility: Visibility,
336 #[serde(default)]
337 pub folder_id: Option<Uuid>,
338 pub folder_path: String,
339 pub filename: String,
340 pub media_type: String,
341 pub size_bytes: i64,
342 pub sha256: String,
343 #[serde(default)]
344 pub source_available: bool,
345 pub ingest_status: LibraryIngestStatus,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub error_message: Option<String>,
348 pub created_at: DateTime<Utc>,
349 pub updated_at: DateTime<Utc>,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub ingested_at: Option<DateTime<Utc>>,
352 pub sections: Vec<LibraryDocumentSectionPreview>,
353 pub jobs: Vec<LibraryIngestJobResponse>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
357pub struct LibraryUploadResponse {
358 pub files: Vec<LibraryFileSummary>,
359 pub jobs: Vec<LibraryIngestJobResponse>,
360}
361
362#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
363pub struct LibraryFileUploadMetadata {
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub external_id: Option<String>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub source_uri: Option<String>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub published_at: Option<DateTime<Utc>>,
370 #[serde(default = "default_metadata_json")]
371 #[schema(value_type = Object)]
372 pub metadata_json: Value,
373}
374
375#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
376pub struct LibraryFileIngestOptions {
377 #[serde(flatten)]
378 pub metadata: LibraryFileUploadMetadata,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub translation: Option<crate::TranslationDirective>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
384pub struct PrepareLibraryUploadRequest {
385 #[serde(default)]
386 pub folder_id: Option<Uuid>,
387 pub filename: String,
388 pub media_type: String,
389 pub size_bytes: i64,
390 pub sha256: String,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub metadata: Option<LibraryFileUploadMetadata>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub translation: Option<crate::TranslationDirective>,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
398pub struct PrepareLibraryUploadResponse {
399 pub upload_required: bool,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub file: Option<LibraryFileSummary>,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub job: Option<LibraryIngestJobResponse>,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
407pub struct ImportLibraryFileFromUrlRequest {
408 pub url: String,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub folder_id: Option<Uuid>,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub filename: Option<String>,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub media_type: Option<String>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub metadata: Option<LibraryFileUploadMetadata>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub translation: Option<crate::TranslationDirective>,
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
422#[serde(rename_all = "snake_case")]
423pub enum LibraryUrlImportStatus {
424 Queued,
425 Downloading,
426 Ingesting,
427 Succeeded,
428 Failed,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
432pub struct LibraryUrlImportJobResponse {
433 pub import_job_id: Uuid,
434 pub group_path: String,
435 pub source_url: String,
436 pub status: LibraryUrlImportStatus,
437 pub attempt_count: i32,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub file: Option<LibraryFileSummary>,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub ingest_job: Option<LibraryIngestJobResponse>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub error_code: Option<String>,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub error_message: Option<String>,
446 pub created_at: DateTime<Utc>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub started_at: Option<DateTime<Utc>>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub finished_at: Option<DateTime<Utc>>,
451 pub updated_at: DateTime<Utc>,
452}
453
454fn default_preview_content_format() -> LibraryPreviewContentFormat {
455 LibraryPreviewContentFormat::PlainText
456}
457
458fn default_text_content_format() -> LibraryTextContentFormat {
459 LibraryTextContentFormat::PlainText
460}
461
462fn default_metadata_json() -> Value {
463 json!({})
464}