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