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, 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().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().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().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().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().await?;
69        create_workspace(&mut conn, name).await
70    }
71
72    pub async fn rename_workspace(&self, workspace_ref: &str, new_name: &str) -> Result<Workspace> {
73        let mut conn = self.acquire().await?;
74        rename_workspace(&mut conn, workspace_ref, new_name).await
75    }
76}
77
78pub(crate) async fn ensure_default_workspace(conn: &mut SqliteConnection) -> Result<Workspace> {
79    if let Some(row) = sqlx::query("SELECT id, key, name FROM workspaces WHERE id = ?")
80        .bind(DEFAULT_WORKSPACE_ID)
81        .fetch_optional(&mut *conn)
82        .await?
83    {
84        return Ok(Workspace {
85            id: row.get("id"),
86            key: row.get("key"),
87            name: row.get("name"),
88        });
89    }
90    let id: WorkspaceId = DEFAULT_WORKSPACE_ID
91        .parse()
92        .expect("valid default workspace ID");
93    let ts = now();
94    sqlx::query("INSERT INTO workspaces(id, name, key, created_at, updated_at) VALUES (?, 'default', 'default', ?, ?)")
95        .bind(&id)
96        .bind(&ts)
97        .bind(&ts)
98        .execute(&mut *conn)
99        .await?;
100    Ok(Workspace {
101        id,
102        key: "default".to_string(),
103        name: "default".to_string(),
104    })
105}
106
107pub(crate) async fn list_workspaces(conn: &mut SqliteConnection) -> Result<Vec<Workspace>> {
108    let rows = sqlx::query("SELECT id, key, name FROM workspaces WHERE archived = 0 ORDER BY key")
109        .fetch_all(&mut *conn)
110        .await?;
111    Ok(rows
112        .into_iter()
113        .map(|row| Workspace {
114            id: row.get("id"),
115            key: row.get("key"),
116            name: row.get("name"),
117        })
118        .collect())
119}
120
121pub(crate) async fn find_workspace(
122    conn: &mut SqliteConnection,
123    name_or_key: &str,
124) -> Result<Option<Workspace>> {
125    let key = normalize_key(name_or_key);
126    let row = sqlx::query(
127        "SELECT id, key, name FROM workspaces
128         WHERE archived = 0 AND (key = ? OR lower(name) = lower(?))",
129    )
130    .bind(key)
131    .bind(name_or_key)
132    .fetch_optional(&mut *conn)
133    .await?;
134    Ok(row.map(workspace_from_row))
135}
136
137pub(crate) async fn workspace_for_id(
138    conn: &mut SqliteConnection,
139    workspace_id: &WorkspaceId,
140) -> Result<Workspace> {
141    let row = sqlx::query("SELECT id, key, name FROM workspaces WHERE id = ? AND archived = 0")
142        .bind(workspace_id)
143        .fetch_optional(&mut *conn)
144        .await?;
145    match row {
146        Some(row) => Ok(workspace_from_row(row)),
147        None => bail!("error unknown-workspace-id id={workspace_id}"),
148    }
149}
150
151pub(crate) async fn workspace_key_for_id(
152    conn: &mut SqliteConnection,
153    workspace_id: &WorkspaceId,
154) -> Result<String> {
155    Ok(workspace_for_id(conn, workspace_id).await?.key)
156}
157
158fn workspace_from_row(row: sqlx::sqlite::SqliteRow) -> Workspace {
159    Workspace {
160        id: row.get("id"),
161        key: row.get("key"),
162        name: row.get("name"),
163    }
164}
165
166pub(crate) async fn resolve_workspace(
167    conn: &mut SqliteConnection,
168    name_or_key: &str,
169) -> Result<Workspace> {
170    resolve_required_workspace(conn, name_or_key, "workspace").await
171}
172
173pub(crate) async fn resolve_required_workspace(
174    conn: &mut SqliteConnection,
175    name_or_key: &str,
176    source: &str,
177) -> Result<Workspace> {
178    if let Some(workspace) = find_workspace(conn, name_or_key).await? {
179        return Ok(workspace);
180    }
181    bail!(
182        "error unknown-workspace input={} source={} hint=\"create the workspace with aven workspace create\"",
183        name_or_key,
184        source
185    );
186}
187
188pub(crate) async fn create_workspace(conn: &mut SqliteConnection, name: &str) -> Result<Workspace> {
189    let key = normalize_key(name);
190    if key.is_empty() {
191        bail!("error invalid-workspace input={name}");
192    }
193    if find_workspace(conn, &key).await?.is_some() {
194        bail!("error workspace-exists key={key}");
195    }
196    let id = WorkspaceId::new();
197    let ts = now();
198    sqlx::query(
199        "INSERT INTO workspaces(id, name, key, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
200    )
201    .bind(&id)
202    .bind(name)
203    .bind(&key)
204    .bind(&ts)
205    .bind(&ts)
206    .execute(&mut *conn)
207    .await?;
208    insert_change(
209        conn,
210        "workspace",
211        id.as_str(),
212        None,
213        "create_workspace",
214        json!({ "key": key, "name": name, "created_at": ts }),
215        None,
216    )
217    .await?;
218    Ok(Workspace {
219        id,
220        key,
221        name: name.to_string(),
222    })
223}
224
225pub(crate) async fn rename_workspace(
226    conn: &mut SqliteConnection,
227    workspace_ref: &str,
228    new_name: &str,
229) -> Result<Workspace> {
230    let workspace = resolve_workspace(conn, workspace_ref).await?;
231    let new_key = normalize_key(new_name);
232    if new_key.is_empty() {
233        bail!("error invalid-workspace input={new_name}");
234    }
235    if new_key != workspace.key && find_workspace(conn, &new_key).await?.is_some() {
236        bail!("error workspace-exists key={new_key}");
237    }
238    let ts = now();
239    sqlx::query("UPDATE workspaces SET name = ?, key = ?, updated_at = ? WHERE id = ?")
240        .bind(new_name)
241        .bind(&new_key)
242        .bind(&ts)
243        .bind(&workspace.id)
244        .execute(&mut *conn)
245        .await?;
246    if workspace.name != new_name {
247        insert_change(
248            conn,
249            "workspace",
250            workspace.id.as_str(),
251            Some("name"),
252            "set_workspace_field",
253            json!({ "value": new_name }),
254            None,
255        )
256        .await?;
257    }
258    if workspace.key != new_key {
259        insert_change(
260            conn,
261            "workspace",
262            workspace.id.as_str(),
263            Some("key"),
264            "set_workspace_field",
265            json!({ "value": new_key }),
266            None,
267        )
268        .await?;
269    }
270    Ok(Workspace {
271        id: workspace.id,
272        key: new_key,
273        name: new_name.to_string(),
274    })
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::db::open_db;
281
282    #[tokio::test]
283    async fn fresh_database_has_default_workspace() {
284        let dir = tempfile::tempdir().unwrap();
285        let pool = open_db(&dir.path().join("test.db")).await.unwrap();
286        let mut conn = pool.acquire().await.unwrap();
287        let workspaces = list_workspaces(&mut conn).await.unwrap();
288        assert_eq!(workspaces.len(), 1);
289        assert_eq!(workspaces[0].key, "default");
290    }
291}