mockforge_collab/
workspace.rs

1//! Workspace management and collaboration
2
3use crate::error::{CollabError, Result};
4use crate::models::{TeamWorkspace, UserRole, WorkspaceMember};
5use crate::permissions::{Permission, PermissionChecker};
6use chrono::Utc;
7use parking_lot::RwLock;
8use sqlx::{Pool, Sqlite};
9use std::collections::HashMap;
10use std::sync::Arc;
11use uuid::Uuid;
12
13/// Workspace service for managing collaborative workspaces
14pub struct WorkspaceService {
15    db: Pool<Sqlite>,
16    cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
17}
18
19impl WorkspaceService {
20    /// Create a new workspace service
21    pub fn new(db: Pool<Sqlite>) -> Self {
22        Self {
23            db,
24            cache: Arc::new(RwLock::new(HashMap::new())),
25        }
26    }
27
28    /// Create a new workspace
29    pub async fn create_workspace(
30        &self,
31        name: String,
32        description: Option<String>,
33        owner_id: Uuid,
34    ) -> Result<TeamWorkspace> {
35        let mut workspace = TeamWorkspace::new(name, owner_id);
36        workspace.description = description;
37
38        // Insert into database
39        sqlx::query!(
40            r#"
41            INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
42            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
43            "#,
44            workspace.id,
45            workspace.name,
46            workspace.description,
47            workspace.owner_id,
48            workspace.config,
49            workspace.version,
50            workspace.created_at,
51            workspace.updated_at,
52            workspace.is_archived
53        )
54        .execute(&self.db)
55        .await?;
56
57        // Add owner as admin member
58        let member = WorkspaceMember::new(workspace.id, owner_id, UserRole::Admin);
59        sqlx::query!(
60            r#"
61            INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
62            VALUES (?, ?, ?, ?, ?, ?)
63            "#,
64            member.id,
65            member.workspace_id,
66            member.user_id,
67            member.role,
68            member.joined_at,
69            member.last_activity
70        )
71        .execute(&self.db)
72        .await?;
73
74        // Update cache
75        self.cache.write().insert(workspace.id, workspace.clone());
76
77        Ok(workspace)
78    }
79
80    /// Get a workspace by ID
81    pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
82        // Check cache first
83        if let Some(workspace) = self.cache.read().get(&workspace_id) {
84            return Ok(workspace.clone());
85        }
86
87        // Query database
88        let workspace = sqlx::query_as!(
89            TeamWorkspace,
90            r#"
91            SELECT
92                id as "id: Uuid",
93                name,
94                description,
95                owner_id as "owner_id: Uuid",
96                config,
97                version,
98                created_at as "created_at: chrono::DateTime<chrono::Utc>",
99                updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
100                is_archived as "is_archived: bool"
101            FROM workspaces
102            WHERE id = ?
103            "#,
104            workspace_id
105        )
106        .fetch_optional(&self.db)
107        .await?
108        .ok_or_else(|| CollabError::WorkspaceNotFound(workspace_id.to_string()))?;
109
110        // Update cache
111        self.cache.write().insert(workspace_id, workspace.clone());
112
113        Ok(workspace)
114    }
115
116    /// Update a workspace
117    pub async fn update_workspace(
118        &self,
119        workspace_id: Uuid,
120        user_id: Uuid,
121        name: Option<String>,
122        description: Option<String>,
123        config: Option<serde_json::Value>,
124    ) -> Result<TeamWorkspace> {
125        // Check permissions
126        let member = self.get_member(workspace_id, user_id).await?;
127        PermissionChecker::check(member.role, Permission::WorkspaceUpdate)?;
128
129        let mut workspace = self.get_workspace(workspace_id).await?;
130
131        // Update fields
132        if let Some(name) = name {
133            workspace.name = name;
134        }
135        if let Some(description) = description {
136            workspace.description = Some(description);
137        }
138        if let Some(config) = config {
139            workspace.config = config;
140        }
141        workspace.updated_at = Utc::now();
142        workspace.version += 1;
143
144        // Save to database
145        sqlx::query!(
146            r#"
147            UPDATE workspaces
148            SET name = ?, description = ?, config = ?, version = ?, updated_at = ?
149            WHERE id = ?
150            "#,
151            workspace.name,
152            workspace.description,
153            workspace.config,
154            workspace.version,
155            workspace.updated_at,
156            workspace.id
157        )
158        .execute(&self.db)
159        .await?;
160
161        // Update cache
162        self.cache.write().insert(workspace_id, workspace.clone());
163
164        Ok(workspace)
165    }
166
167    /// Delete (archive) a workspace
168    pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
169        // Check permissions
170        let member = self.get_member(workspace_id, user_id).await?;
171        PermissionChecker::check(member.role, Permission::WorkspaceDelete)?;
172
173        let now = Utc::now();
174        sqlx::query!(
175            r#"
176            UPDATE workspaces
177            SET is_archived = TRUE, updated_at = ?
178            WHERE id = ?
179            "#,
180            now,
181            workspace_id
182        )
183        .execute(&self.db)
184        .await?;
185
186        // Remove from cache
187        self.cache.write().remove(&workspace_id);
188
189        Ok(())
190    }
191
192    /// Add a member to a workspace
193    pub async fn add_member(
194        &self,
195        workspace_id: Uuid,
196        user_id: Uuid,
197        new_member_id: Uuid,
198        role: UserRole,
199    ) -> Result<WorkspaceMember> {
200        // Check permissions
201        let member = self.get_member(workspace_id, user_id).await?;
202        PermissionChecker::check(member.role, Permission::InviteMembers)?;
203
204        // Create new member
205        let new_member = WorkspaceMember::new(workspace_id, new_member_id, role);
206
207        sqlx::query!(
208            r#"
209            INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
210            VALUES (?, ?, ?, ?, ?, ?)
211            "#,
212            new_member.id,
213            new_member.workspace_id,
214            new_member.user_id,
215            new_member.role,
216            new_member.joined_at,
217            new_member.last_activity
218        )
219        .execute(&self.db)
220        .await?;
221
222        Ok(new_member)
223    }
224
225    /// Remove a member from a workspace
226    pub async fn remove_member(
227        &self,
228        workspace_id: Uuid,
229        user_id: Uuid,
230        member_to_remove: Uuid,
231    ) -> Result<()> {
232        // Check permissions
233        let member = self.get_member(workspace_id, user_id).await?;
234        PermissionChecker::check(member.role, Permission::RemoveMembers)?;
235
236        // Don't allow removing the owner
237        let workspace = self.get_workspace(workspace_id).await?;
238        if member_to_remove == workspace.owner_id {
239            return Err(CollabError::InvalidInput("Cannot remove workspace owner".to_string()));
240        }
241
242        sqlx::query!(
243            r#"
244            DELETE FROM workspace_members
245            WHERE workspace_id = ? AND user_id = ?
246            "#,
247            workspace_id,
248            member_to_remove
249        )
250        .execute(&self.db)
251        .await?;
252
253        Ok(())
254    }
255
256    /// Change a member's role
257    pub async fn change_role(
258        &self,
259        workspace_id: Uuid,
260        user_id: Uuid,
261        member_id: Uuid,
262        new_role: UserRole,
263    ) -> Result<WorkspaceMember> {
264        // Check permissions
265        let member = self.get_member(workspace_id, user_id).await?;
266        PermissionChecker::check(member.role, Permission::ChangeRoles)?;
267
268        // Don't allow changing the owner's role
269        let workspace = self.get_workspace(workspace_id).await?;
270        if member_id == workspace.owner_id {
271            return Err(CollabError::InvalidInput(
272                "Cannot change workspace owner's role".to_string(),
273            ));
274        }
275
276        sqlx::query!(
277            r#"
278            UPDATE workspace_members
279            SET role = ?
280            WHERE workspace_id = ? AND user_id = ?
281            "#,
282            new_role,
283            workspace_id,
284            member_id
285        )
286        .execute(&self.db)
287        .await?;
288
289        self.get_member(workspace_id, member_id).await
290    }
291
292    /// Get a workspace member
293    pub async fn get_member(&self, workspace_id: Uuid, user_id: Uuid) -> Result<WorkspaceMember> {
294        sqlx::query_as!(
295            WorkspaceMember,
296            r#"
297            SELECT
298                id as "id: Uuid",
299                workspace_id as "workspace_id: Uuid",
300                user_id as "user_id: Uuid",
301                role as "role: UserRole",
302                joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
303                last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
304            FROM workspace_members
305            WHERE workspace_id = ? AND user_id = ?
306            "#,
307            workspace_id,
308            user_id
309        )
310        .fetch_optional(&self.db)
311        .await?
312        .ok_or_else(|| CollabError::AuthorizationFailed("User is not a member".to_string()))
313    }
314
315    /// List all members of a workspace
316    pub async fn list_members(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMember>> {
317        let members = sqlx::query_as!(
318            WorkspaceMember,
319            r#"
320            SELECT
321                id as "id: Uuid",
322                workspace_id as "workspace_id: Uuid",
323                user_id as "user_id: Uuid",
324                role as "role: UserRole",
325                joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
326                last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
327            FROM workspace_members
328            WHERE workspace_id = ?
329            ORDER BY joined_at
330            "#,
331            workspace_id
332        )
333        .fetch_all(&self.db)
334        .await?;
335
336        Ok(members)
337    }
338
339    /// List all workspaces for a user
340    pub async fn list_user_workspaces(&self, user_id: Uuid) -> Result<Vec<TeamWorkspace>> {
341        let workspaces = sqlx::query_as!(
342            TeamWorkspace,
343            r#"
344            SELECT
345                w.id as "id: Uuid",
346                w.name,
347                w.description,
348                w.owner_id as "owner_id: Uuid",
349                w.config,
350                w.version,
351                w.created_at as "created_at: chrono::DateTime<chrono::Utc>",
352                w.updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
353                w.is_archived as "is_archived: bool"
354            FROM workspaces w
355            INNER JOIN workspace_members m ON w.id = m.workspace_id
356            WHERE m.user_id = ? AND w.is_archived = FALSE
357            ORDER BY w.updated_at DESC
358            "#,
359            user_id
360        )
361        .fetch_all(&self.db)
362        .await?;
363
364        Ok(workspaces)
365    }
366}
367
368/// Workspace manager (higher-level API)
369pub struct WorkspaceManager {
370    service: Arc<WorkspaceService>,
371}
372
373impl WorkspaceManager {
374    /// Create a new workspace manager
375    pub fn new(service: Arc<WorkspaceService>) -> Self {
376        Self { service }
377    }
378
379    /// Create and setup a new workspace
380    pub async fn create_workspace(
381        &self,
382        name: String,
383        description: Option<String>,
384        owner_id: Uuid,
385    ) -> Result<TeamWorkspace> {
386        self.service.create_workspace(name, description, owner_id).await
387    }
388
389    /// Get workspace with member check
390    pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
391        // Verify user is a member
392        self.service.get_member(workspace_id, user_id).await?;
393        self.service.get_workspace(workspace_id).await
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    // Note: These tests would require a database setup
402    // For now, they serve as documentation of the API
403}