Skip to main content

aven_core/
workspaces.rs

1use crate::ids::WorkspaceId;
2use anyhow::{Result, bail};
3use serde_json::json;
4use sqlx::{Row, SqliteConnection};
5
6use crate::db::{Database, begin_immediate, insert_change};
7use crate::ids::now;
8use crate::projects::normalize_key;
9
10pub const DEFAULT_WORKSPACE_ID: &str = "0000000000000000";
11
12pub fn default_workspace_id() -> WorkspaceId {
13    DEFAULT_WORKSPACE_ID
14        .parse()
15        .expect("valid default workspace ID")
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Workspace {
20    pub id: WorkspaceId,
21    pub key: String,
22    pub name: String,
23}
24
25impl Default for Workspace {
26    fn default() -> Self {
27        Self {
28            id: DEFAULT_WORKSPACE_ID
29                .parse()
30                .expect("valid default workspace ID"),
31            key: "default".to_string(),
32            name: "default".to_string(),
33        }
34    }
35}
36
37impl Database {
38    pub async fn list_workspaces(&self) -> Result<Vec<Workspace>> {
39        let mut conn = self.acquire_reader().await?;
40        list_workspaces(&mut conn).await
41    }
42
43    pub async fn find_workspace(&self, name_or_key: &str) -> Result<Option<Workspace>> {
44        let mut conn = self.acquire_reader().await?;
45        find_workspace(&mut conn, name_or_key).await
46    }
47
48    pub async fn workspace_for_id(&self, workspace_id: &WorkspaceId) -> Result<Workspace> {
49        let mut conn = self.acquire_reader().await?;
50        workspace_for_id(&mut conn, workspace_id).await
51    }
52
53    pub async fn resolve_workspace(&self, name_or_key: &str) -> Result<Workspace> {
54        self.resolve_required_workspace(name_or_key, "workspace")
55            .await
56    }
57
58    pub async fn resolve_required_workspace(
59        &self,
60        name_or_key: &str,
61        source: &str,
62    ) -> Result<Workspace> {
63        let mut conn = self.acquire_reader().await?;
64        resolve_required_workspace(&mut conn, name_or_key, source).await
65    }
66
67    pub async fn create_workspace(&self, name: &str) -> Result<Workspace> {
68        let mut conn = self.acquire_writer().await?;
69        let mut tx = begin_immediate(&mut conn).await?;
70        let workspace = create_workspace(&mut tx, name).await?;
71        tx.commit().await?;
72        Ok(workspace)
73    }
74
75    pub async fn rename_workspace(&self, workspace_ref: &str, new_name: &str) -> Result<Workspace> {
76        let mut conn = self.acquire_writer().await?;
77        let mut tx = begin_immediate(&mut conn).await?;
78        let workspace = rename_workspace(&mut tx, workspace_ref, new_name).await?;
79        tx.commit().await?;
80        Ok(workspace)
81    }
82}
83
84pub(crate) async fn ensure_default_workspace(conn: &mut SqliteConnection) -> Result<Workspace> {
85    if let Some(row) = sqlx::query("SELECT id, key, name FROM workspaces WHERE id = ?")
86        .bind(DEFAULT_WORKSPACE_ID)
87        .fetch_optional(&mut *conn)
88        .await?
89    {
90        return Ok(Workspace {
91            id: row.get("id"),
92            key: row.get("key"),
93            name: row.get("name"),
94        });
95    }
96    let id: WorkspaceId = DEFAULT_WORKSPACE_ID
97        .parse()
98        .expect("valid default workspace ID");
99    let ts = now();
100    sqlx::query("INSERT INTO workspaces(id, name, key, created_at, updated_at) VALUES (?, 'default', 'default', ?, ?)")
101        .bind(&id)
102        .bind(&ts)
103        .bind(&ts)
104        .execute(&mut *conn)
105        .await?;
106    Ok(Workspace {
107        id,
108        key: "default".to_string(),
109        name: "default".to_string(),
110    })
111}
112
113pub(crate) async fn list_workspaces(conn: &mut SqliteConnection) -> Result<Vec<Workspace>> {
114    let rows = sqlx::query("SELECT id, key, name FROM workspaces WHERE archived = 0 ORDER BY key")
115        .fetch_all(&mut *conn)
116        .await?;
117    Ok(rows
118        .into_iter()
119        .map(|row| Workspace {
120            id: row.get("id"),
121            key: row.get("key"),
122            name: row.get("name"),
123        })
124        .collect())
125}
126
127pub(crate) async fn find_workspace(
128    conn: &mut SqliteConnection,
129    name_or_key: &str,
130) -> Result<Option<Workspace>> {
131    let key = normalize_key(name_or_key);
132    let row = sqlx::query(
133        "SELECT id, key, name FROM workspaces
134         WHERE archived = 0 AND (key = ? OR lower(name) = lower(?))",
135    )
136    .bind(key)
137    .bind(name_or_key)
138    .fetch_optional(&mut *conn)
139    .await?;
140    Ok(row.map(workspace_from_row))
141}
142
143pub(crate) async fn workspace_for_id(
144    conn: &mut SqliteConnection,
145    workspace_id: &WorkspaceId,
146) -> Result<Workspace> {
147    let row = sqlx::query("SELECT id, key, name FROM workspaces WHERE id = ? AND archived = 0")
148        .bind(workspace_id)
149        .fetch_optional(&mut *conn)
150        .await?;
151    match row {
152        Some(row) => Ok(workspace_from_row(row)),
153        None => Err(crate::error::CoreError::not_found(format!(
154            "error unknown-workspace-id id={workspace_id}"
155        ))
156        .into()),
157    }
158}
159
160pub(crate) async fn workspace_key_for_id(
161    conn: &mut SqliteConnection,
162    workspace_id: &WorkspaceId,
163) -> Result<String> {
164    Ok(workspace_for_id(conn, workspace_id).await?.key)
165}
166
167fn workspace_from_row(row: sqlx::sqlite::SqliteRow) -> Workspace {
168    Workspace {
169        id: row.get("id"),
170        key: row.get("key"),
171        name: row.get("name"),
172    }
173}
174
175pub(crate) async fn resolve_workspace(
176    conn: &mut SqliteConnection,
177    name_or_key: &str,
178) -> Result<Workspace> {
179    resolve_required_workspace(conn, name_or_key, "workspace").await
180}
181
182pub(crate) async fn resolve_required_workspace(
183    conn: &mut SqliteConnection,
184    name_or_key: &str,
185    source: &str,
186) -> Result<Workspace> {
187    if let Some(workspace) = find_workspace(conn, name_or_key).await? {
188        return Ok(workspace);
189    }
190    bail!(
191        "error unknown-workspace input={} source={} hint=\"create the workspace with aven workspace create\"",
192        name_or_key,
193        source
194    );
195}
196
197pub(crate) async fn create_workspace(conn: &mut SqliteConnection, name: &str) -> Result<Workspace> {
198    let key = normalize_key(name);
199    if key.is_empty() {
200        bail!("error invalid-workspace input={name}");
201    }
202    if find_workspace(conn, &key).await?.is_some() {
203        bail!("error workspace-exists key={key}");
204    }
205    let id = WorkspaceId::new();
206    let ts = now();
207    sqlx::query(
208        "INSERT INTO workspaces(id, name, key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
209    )
210    .bind(&id)
211    .bind(name)
212    .bind(&key)
213    .bind(&ts)
214    .bind(&ts)
215    .execute(&mut *conn)
216    .await?;
217    insert_change(
218        conn,
219        "workspace",
220        id.as_str(),
221        None,
222        "create_workspace",
223        json!({ "key": key, "name": name, "created_at": ts }),
224        None,
225    )
226    .await?;
227    Ok(Workspace {
228        id,
229        key,
230        name: name.to_string(),
231    })
232}
233
234pub(crate) async fn rename_workspace(
235    conn: &mut SqliteConnection,
236    workspace_ref: &str,
237    new_name: &str,
238) -> Result<Workspace> {
239    let workspace = resolve_workspace(conn, workspace_ref).await?;
240    let new_key = normalize_key(new_name);
241    if new_key.is_empty() {
242        bail!("error invalid-workspace input={new_name}");
243    }
244    if new_key != workspace.key && find_workspace(conn, &new_key).await?.is_some() {
245        bail!("error workspace-exists key={new_key}");
246    }
247    let ts = now();
248    sqlx::query("UPDATE workspaces SET name = ?, key = ?, updated_at = ? WHERE id = ?")
249        .bind(new_name)
250        .bind(&new_key)
251        .bind(&ts)
252        .bind(&workspace.id)
253        .execute(&mut *conn)
254        .await?;
255    if workspace.name != new_name {
256        insert_change(
257            conn,
258            "workspace",
259            workspace.id.as_str(),
260            Some("name"),
261            "set_workspace_field",
262            json!({ "value": new_name }),
263            None,
264        )
265        .await?;
266    }
267    if workspace.key != new_key {
268        insert_change(
269            conn,
270            "workspace",
271            workspace.id.as_str(),
272            Some("key"),
273            "set_workspace_field",
274            json!({ "value": new_key }),
275            None,
276        )
277        .await?;
278    }
279    Ok(Workspace {
280        id: workspace.id,
281        key: new_key,
282        name: new_name.to_string(),
283    })
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use crate::db::open_db;
290
291    #[tokio::test]
292    async fn fresh_database_has_default_workspace() {
293        let dir = tempfile::tempdir().unwrap();
294        let pool = open_db(&dir.path().join("test.db")).await.unwrap();
295        let mut conn = pool.acquire().await.unwrap();
296        let workspaces = list_workspaces(&mut conn).await.unwrap();
297        assert_eq!(workspaces.len(), 1);
298        assert_eq!(workspaces[0].key, "default");
299    }
300}