1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::{FromRow, SqlitePool};
4use ts_rs_forge::TS;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
8pub struct Image {
9 pub id: Uuid,
10 pub file_path: String, pub original_name: String,
12 pub mime_type: Option<String>,
13 pub size_bytes: i64,
14 pub hash: String, pub created_at: DateTime<Utc>,
16 pub updated_at: DateTime<Utc>,
17}
18
19#[derive(Debug, Deserialize, TS)]
20pub struct CreateImage {
21 pub file_path: String,
22 pub original_name: String,
23 pub mime_type: Option<String>,
24 pub size_bytes: i64,
25 pub hash: String,
26}
27
28#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
29pub struct TaskImage {
30 pub id: Uuid,
31 pub task_id: Uuid,
32 pub image_id: Uuid,
33 pub created_at: DateTime<Utc>,
34}
35
36#[derive(Debug, Deserialize, TS)]
37pub struct CreateTaskImage {
38 pub task_id: Uuid,
39 pub image_id: Uuid,
40}
41
42impl Image {
43 pub async fn create(pool: &SqlitePool, data: &CreateImage) -> Result<Self, sqlx::Error> {
44 let id = Uuid::new_v4();
45 sqlx::query_as!(
46 Image,
47 r#"INSERT INTO images (id, file_path, original_name, mime_type, size_bytes, hash)
48 VALUES ($1, $2, $3, $4, $5, $6)
49 RETURNING id as "id!: Uuid",
50 file_path as "file_path!",
51 original_name as "original_name!",
52 mime_type,
53 size_bytes as "size_bytes!",
54 hash as "hash!",
55 created_at as "created_at!: DateTime<Utc>",
56 updated_at as "updated_at!: DateTime<Utc>""#,
57 id,
58 data.file_path,
59 data.original_name,
60 data.mime_type,
61 data.size_bytes,
62 data.hash,
63 )
64 .fetch_one(pool)
65 .await
66 }
67
68 pub async fn find_by_hash(pool: &SqlitePool, hash: &str) -> Result<Option<Self>, sqlx::Error> {
69 sqlx::query_as!(
70 Image,
71 r#"SELECT id as "id!: Uuid",
72 file_path as "file_path!",
73 original_name as "original_name!",
74 mime_type,
75 size_bytes as "size_bytes!",
76 hash as "hash!",
77 created_at as "created_at!: DateTime<Utc>",
78 updated_at as "updated_at!: DateTime<Utc>"
79 FROM images
80 WHERE hash = $1"#,
81 hash
82 )
83 .fetch_optional(pool)
84 .await
85 }
86
87 pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
88 sqlx::query_as!(
89 Image,
90 r#"SELECT id as "id!: Uuid",
91 file_path as "file_path!",
92 original_name as "original_name!",
93 mime_type,
94 size_bytes as "size_bytes!",
95 hash as "hash!",
96 created_at as "created_at!: DateTime<Utc>",
97 updated_at as "updated_at!: DateTime<Utc>"
98 FROM images
99 WHERE id = $1"#,
100 id
101 )
102 .fetch_optional(pool)
103 .await
104 }
105
106 pub async fn find_by_task_id(
107 pool: &SqlitePool,
108 task_id: Uuid,
109 ) -> Result<Vec<Self>, sqlx::Error> {
110 sqlx::query_as!(
111 Image,
112 r#"SELECT i.id as "id!: Uuid",
113 i.file_path as "file_path!",
114 i.original_name as "original_name!",
115 i.mime_type,
116 i.size_bytes as "size_bytes!",
117 i.hash as "hash!",
118 i.created_at as "created_at!: DateTime<Utc>",
119 i.updated_at as "updated_at!: DateTime<Utc>"
120 FROM images i
121 JOIN task_images ti ON i.id = ti.image_id
122 WHERE ti.task_id = $1
123 ORDER BY ti.created_at"#,
124 task_id
125 )
126 .fetch_all(pool)
127 .await
128 }
129
130 pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<(), sqlx::Error> {
131 sqlx::query!(r#"DELETE FROM images WHERE id = $1"#, id)
132 .execute(pool)
133 .await?;
134 Ok(())
135 }
136
137 pub async fn find_orphaned_images(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
138 sqlx::query_as!(
139 Image,
140 r#"SELECT i.id as "id!: Uuid",
141 i.file_path as "file_path!",
142 i.original_name as "original_name!",
143 i.mime_type,
144 i.size_bytes as "size_bytes!",
145 i.hash as "hash!",
146 i.created_at as "created_at!: DateTime<Utc>",
147 i.updated_at as "updated_at!: DateTime<Utc>"
148 FROM images i
149 LEFT JOIN task_images ti ON i.id = ti.image_id
150 WHERE ti.task_id IS NULL"#
151 )
152 .fetch_all(pool)
153 .await
154 }
155}
156
157impl TaskImage {
158 pub async fn create(pool: &SqlitePool, data: &CreateTaskImage) -> Result<Self, sqlx::Error> {
159 let id = Uuid::new_v4();
160 sqlx::query_as!(
161 TaskImage,
162 r#"INSERT INTO task_images (id, task_id, image_id)
163 VALUES ($1, $2, $3)
164 RETURNING id as "id!: Uuid",
165 task_id as "task_id!: Uuid",
166 image_id as "image_id!: Uuid",
167 created_at as "created_at!: DateTime<Utc>""#,
168 id,
169 data.task_id,
170 data.image_id,
171 )
172 .fetch_one(pool)
173 .await
174 }
175
176 pub async fn associate_many(
177 pool: &SqlitePool,
178 task_id: Uuid,
179 image_ids: &[Uuid],
180 ) -> Result<(), sqlx::Error> {
181 for &image_id in image_ids {
182 let task_image = CreateTaskImage { task_id, image_id };
183 TaskImage::create(pool, &task_image).await?;
184 }
185 Ok(())
186 }
187
188 pub async fn associate_many_dedup(
190 pool: &SqlitePool,
191 task_id: Uuid,
192 image_ids: &[Uuid],
193 ) -> Result<(), sqlx::Error> {
194 for &image_id in image_ids {
195 let id = Uuid::new_v4();
196 sqlx::query!(
197 r#"INSERT INTO task_images (id, task_id, image_id)
198 SELECT $1, $2, $3
199 WHERE NOT EXISTS (
200 SELECT 1 FROM task_images WHERE task_id = $2 AND image_id = $3
201 )"#,
202 id,
203 task_id,
204 image_id
205 )
206 .execute(pool)
207 .await?;
208 }
209 Ok(())
210 }
211
212 pub async fn delete_by_task_id(pool: &SqlitePool, task_id: Uuid) -> Result<(), sqlx::Error> {
213 sqlx::query!(r#"DELETE FROM task_images WHERE task_id = $1"#, task_id)
214 .execute(pool)
215 .await?;
216 Ok(())
217 }
218}