Skip to main content

doido_storage/
attachments.rs

1//! Polymorphic attachments — the `has_one_attached` / `has_many_attached`
2//! analogue, exposed as helper functions over `(record_type, record_id, name)`.
3//!
4//! `record_type` is the application model name (e.g. `"User"`), `record_id` its
5//! primary key rendered as text, and `name` the attachment slot (e.g. `"avatar"`).
6//! Rows link to blobs by the blob `key`.
7//!
8//! A future proc-macro (`#[has_one_attached]`) can generate typed accessors on
9//! sea-orm models on top of these functions.
10
11use crate::blob::{self, Blob};
12use crate::error::StorageError;
13use doido_core::Result;
14use doido_model::sea_orm::{
15    ConnectionTrait, DatabaseConnection, DbBackend, Statement, Value as DbValue,
16};
17
18fn db_err(e: impl std::fmt::Display) -> StorageError {
19    StorageError::Db(e.to_string())
20}
21
22/// Attach the blob `blob_key` to `record` under `name`. For a `has_one` slot,
23/// callers should [`detach`] first (see [`replace_one`]).
24pub async fn attach(
25    conn: &DatabaseConnection,
26    name: &str,
27    record_type: &str,
28    record_id: &str,
29    blob_key: &str,
30) -> Result<()> {
31    let stmt = Statement::from_sql_and_values(
32        DbBackend::Sqlite,
33        "INSERT INTO storage_attachments (name, record_type, record_id, blob_key, created_at) \
34         VALUES (?, ?, ?, ?, ?)",
35        [
36            DbValue::from(name),
37            DbValue::from(record_type),
38            DbValue::from(record_id),
39            DbValue::from(blob_key),
40            DbValue::from(chrono::Utc::now().to_rfc3339()),
41        ],
42    );
43    conn.execute_raw(stmt).await.map_err(db_err)?;
44    Ok(())
45}
46
47/// `has_one_attached`: replace any existing attachment in the `name` slot with
48/// `blob_key`.
49pub async fn replace_one(
50    conn: &DatabaseConnection,
51    name: &str,
52    record_type: &str,
53    record_id: &str,
54    blob_key: &str,
55) -> Result<()> {
56    detach(conn, name, record_type, record_id).await?;
57    attach(conn, name, record_type, record_id, blob_key).await
58}
59
60/// The blob keys attached to `record` under `name`, oldest first.
61pub async fn attached_keys(
62    conn: &DatabaseConnection,
63    name: &str,
64    record_type: &str,
65    record_id: &str,
66) -> Result<Vec<String>> {
67    let stmt = Statement::from_sql_and_values(
68        DbBackend::Sqlite,
69        "SELECT blob_key FROM storage_attachments \
70         WHERE name = ? AND record_type = ? AND record_id = ? ORDER BY id ASC",
71        [
72            DbValue::from(name),
73            DbValue::from(record_type),
74            DbValue::from(record_id),
75        ],
76    );
77    let rows = conn.query_all_raw(stmt).await.map_err(db_err)?;
78    rows.into_iter()
79        .map(|r| {
80            r.try_get::<String>("", "blob_key")
81                .map_err(|e| db_err(e).into())
82        })
83        .collect()
84}
85
86/// `has_one_attached`: the single attached blob, if any.
87pub async fn one(
88    conn: &DatabaseConnection,
89    name: &str,
90    record_type: &str,
91    record_id: &str,
92) -> Result<Option<Blob>> {
93    let keys = attached_keys(conn, name, record_type, record_id).await?;
94    match keys.first() {
95        Some(key) => blob::find(conn, key).await,
96        None => Ok(None),
97    }
98}
99
100/// `has_many_attached`: all attached blobs.
101pub async fn many(
102    conn: &DatabaseConnection,
103    name: &str,
104    record_type: &str,
105    record_id: &str,
106) -> Result<Vec<Blob>> {
107    let keys = attached_keys(conn, name, record_type, record_id).await?;
108    let mut blobs = Vec::with_capacity(keys.len());
109    for key in keys {
110        if let Some(b) = blob::find(conn, &key).await? {
111            blobs.push(b);
112        }
113    }
114    Ok(blobs)
115}
116
117/// Remove the attachment rows in the `name` slot (does not delete the blobs).
118pub async fn detach(
119    conn: &DatabaseConnection,
120    name: &str,
121    record_type: &str,
122    record_id: &str,
123) -> Result<()> {
124    let stmt = Statement::from_sql_and_values(
125        DbBackend::Sqlite,
126        "DELETE FROM storage_attachments WHERE name = ? AND record_type = ? AND record_id = ?",
127        [
128            DbValue::from(name),
129            DbValue::from(record_type),
130            DbValue::from(record_id),
131        ],
132    );
133    conn.execute_raw(stmt).await.map_err(db_err)?;
134    Ok(())
135}
136
137/// All blob keys attached to a record across every slot — used by
138/// `purge_for_record` to purge dependent blobs when a record is destroyed.
139pub async fn all_keys_for_record(
140    conn: &DatabaseConnection,
141    record_type: &str,
142    record_id: &str,
143) -> Result<Vec<String>> {
144    let stmt = Statement::from_sql_and_values(
145        DbBackend::Sqlite,
146        "SELECT blob_key FROM storage_attachments WHERE record_type = ? AND record_id = ?",
147        [DbValue::from(record_type), DbValue::from(record_id)],
148    );
149    let rows = conn.query_all_raw(stmt).await.map_err(db_err)?;
150    rows.into_iter()
151        .map(|r| {
152            r.try_get::<String>("", "blob_key")
153                .map_err(|e| db_err(e).into())
154        })
155        .collect()
156}
157
158/// Delete every attachment row pointing at `blob_key` (used when purging a blob).
159pub async fn delete_for_blob(conn: &DatabaseConnection, blob_key: &str) -> Result<()> {
160    let stmt = Statement::from_sql_and_values(
161        DbBackend::Sqlite,
162        "DELETE FROM storage_attachments WHERE blob_key = ?",
163        [DbValue::from(blob_key)],
164    );
165    conn.execute_raw(stmt).await.map_err(db_err)?;
166    Ok(())
167}
168
169/// Delete a single attachment row linking `record`/`name` to `blob_key`.
170pub async fn detach_blob(
171    conn: &DatabaseConnection,
172    name: &str,
173    record_type: &str,
174    record_id: &str,
175    blob_key: &str,
176) -> Result<()> {
177    let stmt = Statement::from_sql_and_values(
178        DbBackend::Sqlite,
179        "DELETE FROM storage_attachments \
180         WHERE name = ? AND record_type = ? AND record_id = ? AND blob_key = ?",
181        [
182            DbValue::from(name),
183            DbValue::from(record_type),
184            DbValue::from(record_id),
185            DbValue::from(blob_key),
186        ],
187    );
188    conn.execute_raw(stmt).await.map_err(db_err)?;
189    Ok(())
190}