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}