1use std::path::Path;
4
5use imagegen_bridge_core::{
6 BridgeError, ErrorCode, ImagePreset, ImagePresetCreate, ImagePresetTemplate, ImagePresetWrite,
7 validate_preset_name, validate_preset_write,
8};
9use tokio_rusqlite::{Connection, params, rusqlite::OptionalExtension as _};
10
11type PresetRow = (String, Option<String>, String, u64, u64);
12
13pub struct SqlitePresetStore {
15 connection: Connection,
16}
17
18impl std::fmt::Debug for SqlitePresetStore {
19 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 formatter
21 .debug_struct("SqlitePresetStore")
22 .finish_non_exhaustive()
23 }
24}
25
26impl SqlitePresetStore {
27 pub async fn open(path: &Path) -> Result<Self, BridgeError> {
29 match tokio::fs::symlink_metadata(path).await {
30 Ok(metadata) if metadata.file_type().is_file() => {}
31 Ok(_) => {
32 return Err(preset_storage_error(
33 "preset database must be a regular file",
34 ));
35 }
36 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
37 Err(_) => return Err(preset_storage_error("could not inspect preset database")),
38 }
39 let connection = Connection::open(path)
40 .await
41 .map_err(|_| preset_storage_error("could not open preset database"))?;
42 connection
43 .call(|connection| {
44 connection.execute_batch(
45 "PRAGMA journal_mode=WAL;
46 PRAGMA synchronous=FULL;
47 PRAGMA busy_timeout=5000;
48 CREATE TABLE IF NOT EXISTS image_presets (
49 name TEXT PRIMARY KEY,
50 description TEXT,
51 template_json TEXT NOT NULL,
52 created_at INTEGER NOT NULL,
53 updated_at INTEGER NOT NULL
54 );
55 CREATE INDEX IF NOT EXISTS image_presets_updated_idx
56 ON image_presets(updated_at DESC, name ASC);",
57 )?;
58 Ok::<_, tokio_rusqlite::rusqlite::Error>(())
59 })
60 .await
61 .map_err(|_| preset_storage_error("could not initialize preset database"))?;
62 Ok(Self { connection })
63 }
64
65 pub async fn create(
67 &self,
68 create: ImagePresetCreate,
69 now: u64,
70 ) -> Result<ImagePreset, BridgeError> {
71 validate_preset_name(&create.name)?;
72 let write = ImagePresetWrite {
73 description: create.description,
74 template: create.template,
75 };
76 validate_preset_write(&write)?;
77 let template = encode_template(&write.template)?;
78 let name = create.name;
79 let description = write.description;
80 let stored_name = name.clone();
81 let stored_description = description.clone();
82 let inserted = self
83 .connection
84 .call(move |connection| {
85 connection.execute(
86 "INSERT OR IGNORE INTO image_presets(name,description,template_json,created_at,updated_at)
87 VALUES (?1,?2,?3,?4,?4)",
88 params![stored_name, stored_description, template, now],
89 )
90 })
91 .await
92 .map_err(|_| preset_storage_error("could not create preset"))?;
93 if inserted == 0 {
94 return Err(BridgeError::new(
95 ErrorCode::IdempotencyConflict,
96 "a preset with this name already exists",
97 )
98 .with_detail("field", "name")
99 .with_detail("resource", "preset"));
100 }
101 Ok(ImagePreset {
102 name,
103 description,
104 template: write.template,
105 created: now,
106 updated: now,
107 })
108 }
109
110 pub async fn get(&self, name: &str) -> Result<ImagePreset, BridgeError> {
112 validate_preset_name(name)?;
113 let name = name.to_owned();
114 let row = self
115 .connection
116 .call(move |connection| {
117 connection
118 .query_row(
119 "SELECT name,description,template_json,created_at,updated_at
120 FROM image_presets WHERE name=?1",
121 [name],
122 |row| {
123 Ok((
124 row.get(0)?,
125 row.get(1)?,
126 row.get(2)?,
127 row.get(3)?,
128 row.get(4)?,
129 ))
130 },
131 )
132 .optional()
133 })
134 .await
135 .map_err(|_| preset_storage_error("could not read preset"))?
136 .ok_or_else(preset_not_found)?;
137 decode_row(row)
138 }
139
140 pub async fn list(
142 &self,
143 after: Option<String>,
144 limit: usize,
145 ) -> Result<Vec<ImagePreset>, BridgeError> {
146 let rows = self
147 .connection
148 .call(move |connection| {
149 let mut statement = connection.prepare(
150 "SELECT name,description,template_json,created_at,updated_at
151 FROM image_presets
152 WHERE (?1 IS NULL OR name > ?1)
153 ORDER BY name ASC LIMIT ?2",
154 )?;
155 statement
156 .query_map(params![after, limit], |row| {
157 Ok((
158 row.get(0)?,
159 row.get(1)?,
160 row.get(2)?,
161 row.get(3)?,
162 row.get(4)?,
163 ))
164 })?
165 .collect::<Result<Vec<PresetRow>, _>>()
166 })
167 .await
168 .map_err(|_| preset_storage_error("could not list presets"))?;
169 rows.into_iter().map(decode_row).collect()
170 }
171
172 pub async fn replace(
174 &self,
175 name: &str,
176 write: ImagePresetWrite,
177 now: u64,
178 ) -> Result<ImagePreset, BridgeError> {
179 validate_preset_name(name)?;
180 validate_preset_write(&write)?;
181 let template = encode_template(&write.template)?;
182 let stored_name = name.to_owned();
183 let stored_description = write.description.clone();
184 let updated = self
185 .connection
186 .call(move |connection| {
187 connection.execute(
188 "UPDATE image_presets
189 SET description=?2,template_json=?3,updated_at=?4 WHERE name=?1",
190 params![stored_name, stored_description, template, now],
191 )
192 })
193 .await
194 .map_err(|_| preset_storage_error("could not update preset"))?;
195 if updated == 0 {
196 return Err(preset_not_found());
197 }
198 self.get(name).await
199 }
200
201 pub async fn delete(&self, name: &str) -> Result<(), BridgeError> {
203 validate_preset_name(name)?;
204 let name = name.to_owned();
205 let deleted = self
206 .connection
207 .call(move |connection| {
208 connection.execute("DELETE FROM image_presets WHERE name=?1", [name])
209 })
210 .await
211 .map_err(|_| preset_storage_error("could not delete preset"))?;
212 if deleted == 0 {
213 return Err(preset_not_found());
214 }
215 Ok(())
216 }
217}
218
219fn encode_template(template: &ImagePresetTemplate) -> Result<String, BridgeError> {
220 serde_json::to_string(template).map_err(|_| preset_storage_error("could not encode preset"))
221}
222
223fn decode_row(row: PresetRow) -> Result<ImagePreset, BridgeError> {
224 let (name, description, template, created, updated) = row;
225 let template = serde_json::from_str(&template)
226 .map_err(|_| preset_storage_error("stored preset is invalid"))?;
227 Ok(ImagePreset {
228 name,
229 description,
230 template,
231 created,
232 updated,
233 })
234}
235
236fn preset_not_found() -> BridgeError {
237 BridgeError::new(ErrorCode::InvalidRequest, "preset was not found")
238 .with_detail("resource", "preset")
239}
240
241fn preset_storage_error(message: &'static str) -> BridgeError {
242 BridgeError::new(ErrorCode::Internal, message).with_detail("stage", "preset_storage")
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[tokio::test]
250 async fn preset_crud_is_durable_and_conflict_safe() -> Result<(), Box<dyn std::error::Error>> {
251 let directory = tempfile::tempdir()?;
252 let path = directory.path().join("state.sqlite3");
253 let store = SqlitePresetStore::open(&path).await?;
254 let created = store
255 .create(
256 ImagePresetCreate {
257 name: "portrait".to_owned(),
258 description: Some("Editorial portrait".to_owned()),
259 template: ImagePresetTemplate::default(),
260 },
261 10,
262 )
263 .await?;
264 assert_eq!(created.name, "portrait");
265 assert_eq!(store.list(None, 10).await?.len(), 1);
266 let conflict = store
267 .create(
268 ImagePresetCreate {
269 name: "portrait".to_owned(),
270 description: None,
271 template: ImagePresetTemplate::default(),
272 },
273 11,
274 )
275 .await;
276 assert!(matches!(
277 conflict,
278 Err(ref error) if error.code == ErrorCode::IdempotencyConflict
279 ));
280 let updated = store
281 .replace(
282 "portrait",
283 ImagePresetWrite {
284 description: Some("Updated".to_owned()),
285 template: ImagePresetTemplate::default(),
286 },
287 12,
288 )
289 .await?;
290 assert_eq!(updated.created, 10);
291 assert_eq!(updated.updated, 12);
292 store.delete("portrait").await?;
293 assert!(store.get("portrait").await.is_err());
294 Ok(())
295 }
296}