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}