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(×tamp)
125 .bind(×tamp)
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": ×tamp,
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(×tamp)
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}