Skip to main content

aven_core/metadata/
fields.rs

1use anyhow::{Result, bail};
2use serde_json::json;
3use sqlx::{Row, SqliteConnection};
4
5use crate::db::{
6    Database, begin_immediate, entity_conflict_exists, entity_field_version, insert_change,
7    set_entity_field_version,
8};
9use crate::ids::{MetadataFieldId, WorkspaceId, now};
10use crate::types::MutableEntityType;
11use crate::workspaces::Workspace;
12
13use super::{MetadataField, MetadataFieldUsage};
14
15impl Database {
16    pub async fn list_metadata_fields(
17        &self,
18        workspace_id: &WorkspaceId,
19    ) -> Result<Vec<MetadataFieldUsage>> {
20        let mut conn = self.acquire_reader().await?;
21        list_metadata_fields_in_workspace(&mut conn, workspace_id).await
22    }
23
24    pub async fn find_metadata_field(
25        &self,
26        workspace_id: &WorkspaceId,
27        key: &str,
28    ) -> Result<Option<MetadataField>> {
29        let mut conn = self.acquire_reader().await?;
30        find_metadata_field_in_workspace(&mut conn, workspace_id, key).await
31    }
32
33    pub async fn rename_metadata_field(
34        &self,
35        workspace: &Workspace,
36        key: &str,
37        new_key: &str,
38    ) -> Result<MetadataField> {
39        let mut conn = self.acquire_writer().await?;
40        let mut tx = begin_immediate(&mut conn).await?;
41        let field = rename_metadata_field(&mut tx, workspace, key, new_key).await?;
42        tx.commit().await?;
43        Ok(field)
44    }
45}
46pub fn normalize_metadata_key(input: &str) -> Result<String> {
47    let key = input.trim().to_ascii_lowercase();
48    let mut bytes = key.bytes();
49    let Some(first) = bytes.next() else {
50        bail!("error invalid-metadata-key");
51    };
52    if !first.is_ascii_lowercase()
53        || key.len() > 64
54        || !bytes.all(|byte| {
55            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
56        })
57        || key.starts_with("aven.")
58    {
59        bail!("error invalid-metadata-key");
60    }
61    Ok(key)
62}
63
64pub(crate) async fn find_metadata_field_in_workspace(
65    conn: &mut SqliteConnection,
66    workspace_id: &WorkspaceId,
67    input: &str,
68) -> Result<Option<MetadataField>> {
69    let key = normalize_metadata_key(input)?;
70    metadata_field_by_key(conn, workspace_id, &key).await
71}
72
73pub(crate) async fn metadata_field_by_key(
74    conn: &mut SqliteConnection,
75    workspace_id: &WorkspaceId,
76    key: &str,
77) -> Result<Option<MetadataField>> {
78    let row = sqlx::query(
79        "SELECT id, workspace_id, key, created_at, updated_at
80         FROM metadata_fields WHERE workspace_id = ? AND key = ?",
81    )
82    .bind(workspace_id)
83    .bind(key)
84    .fetch_optional(&mut *conn)
85    .await?;
86    Ok(row.map(metadata_field_from_row))
87}
88
89pub(crate) async fn metadata_field_by_id(
90    conn: &mut SqliteConnection,
91    workspace_id: &WorkspaceId,
92    field_id: &MetadataFieldId,
93) -> Result<Option<MetadataField>> {
94    let row = sqlx::query(
95        "SELECT id, workspace_id, key, created_at, updated_at
96         FROM metadata_fields WHERE workspace_id = ? AND id = ?",
97    )
98    .bind(workspace_id)
99    .bind(field_id)
100    .fetch_optional(&mut *conn)
101    .await?;
102    Ok(row.map(metadata_field_from_row))
103}
104
105pub(crate) async fn resolve_or_create_metadata_field(
106    conn: &mut SqliteConnection,
107    workspace: &Workspace,
108    input: &str,
109) -> Result<MetadataField> {
110    let key = normalize_metadata_key(input)?;
111    if let Some(field) = metadata_field_by_key(conn, &workspace.id, &key).await? {
112        return Ok(field);
113    }
114
115    let id = MetadataFieldId::new();
116    let timestamp = now();
117    sqlx::query(
118        "INSERT INTO metadata_fields(id, workspace_id, key, created_at, updated_at)
119         VALUES (?, ?, ?, ?, ?)",
120    )
121    .bind(&id)
122    .bind(&workspace.id)
123    .bind(&key)
124    .bind(&timestamp)
125    .bind(&timestamp)
126    .execute(&mut *conn)
127    .await?;
128    let change_id = insert_change(
129        conn,
130        "metadata_field",
131        id.as_str(),
132        None,
133        "create_metadata_field",
134        json!({
135            "workspace_id": &workspace.id,
136            "workspace_key": &workspace.key,
137            "key": &key,
138            "created_at": &timestamp,
139        }),
140        None,
141    )
142    .await?;
143    set_entity_field_version(
144        conn,
145        &workspace.id,
146        MutableEntityType::MetadataField,
147        id.as_str(),
148        "key",
149        &change_id,
150    )
151    .await?;
152    Ok(MetadataField {
153        id,
154        workspace_id: workspace.id.clone(),
155        key,
156        created_at: timestamp.clone(),
157        updated_at: timestamp,
158    })
159}
160
161pub(crate) async fn rename_metadata_field(
162    conn: &mut SqliteConnection,
163    workspace: &Workspace,
164    input: &str,
165    new_input: &str,
166) -> Result<MetadataField> {
167    let key = normalize_metadata_key(input)?;
168    let new_key = normalize_metadata_key(new_input)?;
169    let mut field = metadata_field_by_key(conn, &workspace.id, &key)
170        .await?
171        .ok_or_else(|| anyhow::anyhow!("error unknown-metadata-field"))?;
172    let resolving = entity_conflict_exists(
173        conn,
174        &workspace.id,
175        MutableEntityType::MetadataField,
176        field.id.as_str(),
177        "key",
178    )
179    .await?;
180    if key == new_key && !resolving {
181        return Ok(field);
182    }
183    if key != new_key
184        && metadata_field_by_key(conn, &workspace.id, &new_key)
185            .await?
186            .is_some()
187    {
188        bail!("error metadata-field-exists");
189    }
190    let base = if resolving {
191        None
192    } else {
193        entity_field_version(
194            conn,
195            &workspace.id,
196            MutableEntityType::MetadataField,
197            field.id.as_str(),
198            "key",
199        )
200        .await?
201    };
202    let timestamp = now();
203    sqlx::query(
204        "UPDATE metadata_fields SET key = ?, updated_at = ?
205         WHERE workspace_id = ? AND id = ?",
206    )
207    .bind(&new_key)
208    .bind(&timestamp)
209    .bind(&workspace.id)
210    .bind(&field.id)
211    .execute(&mut *conn)
212    .await?;
213    let change_id = insert_change(
214        conn,
215        "metadata_field",
216        field.id.as_str(),
217        Some("key"),
218        "set_metadata_field",
219        json!({
220            "workspace_id": &workspace.id,
221            "workspace_key": &workspace.key,
222            "key": &new_key,
223            "conflict_resolution": resolving,
224        }),
225        base.as_deref(),
226    )
227    .await?;
228    set_entity_field_version(
229        conn,
230        &workspace.id,
231        MutableEntityType::MetadataField,
232        field.id.as_str(),
233        "key",
234        &change_id,
235    )
236    .await?;
237    if resolving {
238        sqlx::query(
239            "UPDATE conflicts SET resolved = 1
240             WHERE workspace_id = ? AND entity_type = 'metadata_field'
241               AND entity_id = ? AND field = 'key' AND resolved = 0",
242        )
243        .bind(&workspace.id)
244        .bind(&field.id)
245        .execute(&mut *conn)
246        .await?;
247    }
248    field.key = new_key;
249    field.updated_at = timestamp;
250    Ok(field)
251}
252pub(crate) async fn require_metadata_field(
253    conn: &mut SqliteConnection,
254    workspace_id: &WorkspaceId,
255    field_id: &MetadataFieldId,
256    key: &str,
257) -> Result<MetadataField> {
258    let field = metadata_field_by_id(conn, workspace_id, field_id).await?;
259    match field {
260        Some(field) if field.key == key => Ok(field),
261        _ => bail!("error metadata-field-changed"),
262    }
263}
264async fn list_metadata_fields_in_workspace(
265    conn: &mut SqliteConnection,
266    workspace_id: &WorkspaceId,
267) -> Result<Vec<MetadataFieldUsage>> {
268    let rows = sqlx::query(
269        "SELECT f.id, f.workspace_id, f.key, f.created_at, f.updated_at,
270                (SELECT count(*) FROM task_metadata m
271                 WHERE m.workspace_id = f.workspace_id AND m.field_id = f.id) AS task_count,
272                (SELECT count(*) FROM recurrence_series_metadata m
273                 WHERE m.workspace_id = f.workspace_id AND m.field_id = f.id) AS series_count
274         FROM metadata_fields f
275         WHERE f.workspace_id = ?
276         ORDER BY f.key",
277    )
278    .bind(workspace_id)
279    .fetch_all(&mut *conn)
280    .await?;
281    Ok(rows
282        .into_iter()
283        .map(|row| MetadataFieldUsage {
284            field: MetadataField {
285                id: row.get("id"),
286                workspace_id: row.get("workspace_id"),
287                key: row.get("key"),
288                created_at: row.get("created_at"),
289                updated_at: row.get("updated_at"),
290            },
291            task_count: usize::try_from(row.get::<i64, _>("task_count")).unwrap_or(usize::MAX),
292            series_count: usize::try_from(row.get::<i64, _>("series_count")).unwrap_or(usize::MAX),
293        })
294        .collect())
295}
296
297fn metadata_field_from_row(row: sqlx::sqlite::SqliteRow) -> MetadataField {
298    MetadataField {
299        id: row.get("id"),
300        workspace_id: row.get("workspace_id"),
301        key: row.get("key"),
302        created_at: row.get("created_at"),
303        updated_at: row.get("updated_at"),
304    }
305}