1use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc};
11
12#[cfg(feature = "openapi")]
13#[cfg(feature = "openapi")]
14use utoipa::ToSchema;
15
16#[cfg(feature = "validation")]
17use validator::Validate;
18
19use crate::domain::entity::FileComment;
20use crate::domain::entity::AuditMetadata;
21use crate::domain::entity::CommentStatus;
22
23#[derive(Debug, Clone, Deserialize)]
32#[cfg_attr(feature = "openapi", derive(ToSchema))]
33#[cfg_attr(feature = "validation", derive(Validate))]
34#[serde(rename_all = "camelCase")]
35pub struct CreateFileCommentDto {
36 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
37 #[serde(alias = "file_id")]
38 pub file_id: Uuid,
39 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
40 #[serde(alias = "user_id")]
41 pub user_id: Uuid,
42 #[serde(default, skip_serializing_if = "Option::is_none", alias = "parent_id")]
43 pub parent_id: Option<Uuid>,
44 #[cfg_attr(feature = "validation", validate(length(max = 10000)))]
45 #[cfg_attr(feature = "openapi", schema(example = "example"))]
46 pub content: String,
47 #[serde(default, skip_serializing_if = "Option::is_none", alias = "annotation_region")]
48 pub annotation_region: Option<serde_json::Value>,
49 pub mentions: Vec<Uuid>,
50 #[cfg_attr(feature = "openapi", schema(example = true))]
51 pub resolved: bool,
52 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_by")]
53 pub resolved_by: Option<Uuid>,
54 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_at")]
55 pub resolved_at: Option<DateTime<Utc>>,
56 pub status: CommentStatus,
57}
58
59#[derive(Debug, Clone, Deserialize)]
68#[cfg_attr(feature = "openapi", derive(ToSchema))]
69#[cfg_attr(feature = "validation", derive(Validate))]
70#[serde(rename_all = "camelCase")]
71pub struct UpdateFileCommentDto {
72 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
73 #[serde(alias = "file_id")]
74 pub file_id: Uuid,
75 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
76 #[serde(alias = "user_id")]
77 pub user_id: Uuid,
78 #[serde(default, skip_serializing_if = "Option::is_none", alias = "parent_id")]
79 pub parent_id: Option<Uuid>,
80 #[cfg_attr(feature = "validation", validate(length(max = 10000)))]
81 #[cfg_attr(feature = "openapi", schema(example = "example"))]
82 pub content: String,
83 #[serde(default, skip_serializing_if = "Option::is_none", alias = "annotation_region")]
84 pub annotation_region: Option<serde_json::Value>,
85 pub mentions: Vec<Uuid>,
86 #[cfg_attr(feature = "openapi", schema(example = true))]
87 pub resolved: bool,
88 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_by")]
89 pub resolved_by: Option<Uuid>,
90 #[serde(default, skip_serializing_if = "Option::is_none", alias = "resolved_at")]
91 pub resolved_at: Option<DateTime<Utc>>,
92 pub status: CommentStatus,
93}
94
95#[derive(Debug, Clone, Default, Deserialize)]
104#[cfg_attr(feature = "openapi", derive(ToSchema))]
105#[cfg_attr(feature = "validation", derive(Validate))]
106#[serde(rename_all = "camelCase")]
107pub struct PatchFileCommentDto {
108 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
109 #[serde(skip_serializing_if = "Option::is_none", alias = "file_id")]
110 pub file_id: Option<Uuid>,
111 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
112 #[serde(skip_serializing_if = "Option::is_none", alias = "user_id")]
113 pub user_id: Option<Uuid>,
114 #[serde(skip_serializing_if = "Option::is_none", alias = "parent_id")]
115 pub parent_id: Option<Uuid>,
116 #[cfg_attr(feature = "validation", validate(length(max = 10000)))]
117 #[cfg_attr(feature = "openapi", schema(example = "example"))]
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub content: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none", alias = "annotation_region")]
121 pub annotation_region: Option<serde_json::Value>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub mentions: Option<Vec<Uuid>>,
124 #[cfg_attr(feature = "openapi", schema(example = true))]
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub resolved: Option<bool>,
127 #[serde(skip_serializing_if = "Option::is_none", alias = "resolved_by")]
128 pub resolved_by: Option<Uuid>,
129 #[serde(skip_serializing_if = "Option::is_none", alias = "resolved_at")]
130 pub resolved_at: Option<DateTime<Utc>>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub status: Option<CommentStatus>,
133}
134
135impl PatchFileCommentDto {
136 pub fn has_changes(&self) -> bool {
138 self.file_id.is_some() || self.user_id.is_some() || self.parent_id.is_some() || self.content.is_some() || self.annotation_region.is_some() || self.mentions.is_some() || self.resolved.is_some() || self.resolved_by.is_some() || self.resolved_at.is_some() || self.status.is_some()
139 }
140}
141
142#[derive(Debug, Clone, Serialize)]
151#[cfg_attr(feature = "openapi", derive(ToSchema))]
152#[serde(rename_all = "camelCase")]
153pub struct FileCommentResponseDto {
154 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
155 pub id: Uuid,
156 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
157 pub file_id: Uuid,
158 #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
159 pub user_id: Uuid,
160 pub parent_id: Option<Uuid>,
161 #[cfg_attr(feature = "openapi", schema(example = "example"))]
162 pub content: String,
163 pub annotation_region: Option<serde_json::Value>,
164 pub mentions: Vec<Uuid>,
165 #[cfg_attr(feature = "openapi", schema(example = true))]
166 pub resolved: bool,
167 pub resolved_by: Option<Uuid>,
168 pub resolved_at: Option<DateTime<Utc>>,
169 pub status: CommentStatus,
170 pub metadata: AuditMetadata,
171}
172
173#[derive(Debug, Clone, Serialize)]
182#[cfg_attr(feature = "openapi", derive(ToSchema))]
183#[serde(rename_all = "camelCase")]
184pub struct FileCommentListResponseDto {
185 pub items: Vec<FileCommentResponseDto>,
187 pub total: u64,
189 pub page: u32,
191 pub per_page: u32,
193 pub total_pages: u32,
195 pub has_next: bool,
197 pub has_prev: bool,
199}
200
201impl FileCommentListResponseDto {
202 pub fn new(items: Vec<FileCommentResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
204 let total_pages = if per_page > 0 {
205 ((total as f64) / (per_page as f64)).ceil() as u32
206 } else {
207 0
208 };
209 Self {
210 items,
211 total,
212 page,
213 per_page,
214 total_pages,
215 has_next: page < total_pages,
216 has_prev: page > 1,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Serialize)]
223#[cfg_attr(feature = "openapi", derive(ToSchema))]
224#[serde(rename_all = "camelCase")]
225pub struct FileCommentSummaryDto {
226 pub id: Uuid,
227 pub file_id: Uuid,
228 pub user_id: Uuid,
229 pub parent_id: Option<Uuid>,
230 pub created_at: Option<DateTime<Utc>>,
231}
232
233impl From<FileComment> for FileCommentResponseDto {
238 fn from(entity: FileComment) -> Self {
239 Self {
240 id: entity.id,
241 file_id: entity.file_id,
242 user_id: entity.user_id,
243 parent_id: entity.parent_id,
244 content: entity.content,
245 annotation_region: entity.annotation_region,
246 mentions: entity.mentions,
247 resolved: entity.resolved,
248 resolved_by: entity.resolved_by,
249 resolved_at: entity.resolved_at,
250 status: entity.status,
251 metadata: entity.metadata,
252 }
253 }
254}
255
256impl From<FileComment> for FileCommentSummaryDto {
257 fn from(entity: FileComment) -> Self {
258 let created_at = backbone_core::PersistentEntity::created_at(&entity);
259 Self {
260 id: entity.id,
261 file_id: entity.file_id,
262 user_id: entity.user_id,
263 parent_id: entity.parent_id,
264 created_at,
265 }
266 }
267}
268
269impl From<CreateFileCommentDto> for FileComment {
270 fn from(dto: CreateFileCommentDto) -> Self {
271 Self {
272 id: Uuid::new_v4(),
273 file_id: dto.file_id,
274 user_id: dto.user_id,
275 parent_id: dto.parent_id,
276 content: dto.content,
277 annotation_region: dto.annotation_region,
278 mentions: dto.mentions,
279 resolved: dto.resolved,
280 resolved_by: dto.resolved_by,
281 resolved_at: dto.resolved_at,
282 status: dto.status,
283 metadata: AuditMetadata::default(),
284 }
285 }
286}
287
288impl From<&FileComment> for FileCommentResponseDto {
289 fn from(entity: &FileComment) -> Self {
290 Self {
291 id: entity.id.clone(),
292 file_id: entity.file_id.clone(),
293 user_id: entity.user_id.clone(),
294 parent_id: entity.parent_id.clone(),
295 content: entity.content.clone(),
296 annotation_region: entity.annotation_region.clone(),
297 mentions: entity.mentions.clone(),
298 resolved: entity.resolved.clone(),
299 resolved_by: entity.resolved_by.clone(),
300 resolved_at: entity.resolved_at.clone(),
301 status: entity.status.clone(),
302 metadata: entity.metadata.clone(),
303 }
304 }
305}
306
307impl backbone_core::FromCreateDto<CreateFileCommentDto> for FileComment {
308 fn from_create_dto(dto: CreateFileCommentDto) -> backbone_core::ServiceResult<Self> {
309 Ok(FileComment::from(dto))
310 }
311}
312
313impl backbone_core::ApplyUpdateDto<UpdateFileCommentDto> for FileComment {
314 fn apply_update(mut self, dto: UpdateFileCommentDto) -> backbone_core::ServiceResult<Self> {
315 self.file_id = dto.file_id;
316 self.user_id = dto.user_id;
317 self.parent_id = dto.parent_id;
318 self.content = dto.content;
319 self.annotation_region = dto.annotation_region;
320 self.mentions = dto.mentions;
321 self.resolved = dto.resolved;
322 self.resolved_by = dto.resolved_by;
323 self.resolved_at = dto.resolved_at;
324 self.status = dto.status;
325 Ok(self)
326 }
327}
328
329