mockforge_collab/
workspace.rs

1//! Workspace management and collaboration
2
3use crate::core_bridge::CoreBridge;
4use crate::error::{CollabError, Result};
5use crate::models::{
6    MergeConflict, MergeStatus, TeamWorkspace, UserRole, WorkspaceFork, WorkspaceMember,
7    WorkspaceMerge,
8};
9use crate::permissions::{Permission, PermissionChecker};
10use chrono::Utc;
11use parking_lot::RwLock;
12use sqlx::{Pool, Sqlite};
13use std::collections::HashMap;
14use std::sync::Arc;
15use uuid::Uuid;
16
17/// Workspace service for managing collaborative workspaces
18pub struct WorkspaceService {
19    db: Pool<Sqlite>,
20    cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
21    core_bridge: Option<Arc<CoreBridge>>,
22}
23
24impl WorkspaceService {
25    /// Create a new workspace service
26    pub fn new(db: Pool<Sqlite>) -> Self {
27        Self {
28            db,
29            cache: Arc::new(RwLock::new(HashMap::new())),
30            core_bridge: None,
31        }
32    }
33
34    /// Create a new workspace service with CoreBridge integration
35    pub fn with_core_bridge(db: Pool<Sqlite>, core_bridge: Arc<CoreBridge>) -> Self {
36        Self {
37            db,
38            cache: Arc::new(RwLock::new(HashMap::new())),
39            core_bridge: Some(core_bridge),
40        }
41    }
42
43    /// Create a new workspace
44    pub async fn create_workspace(
45        &self,
46        name: String,
47        description: Option<String>,
48        owner_id: Uuid,
49    ) -> Result<TeamWorkspace> {
50        let mut workspace = TeamWorkspace::new(name.clone(), owner_id);
51        workspace.description = description.clone();
52
53        // If we have CoreBridge, create a proper core workspace and embed it
54        if let Some(core_bridge) = &self.core_bridge {
55            let core_workspace = core_bridge.create_empty_workspace(name, owner_id)?;
56            workspace.config = core_workspace.config;
57        } else {
58            // Fallback: create minimal config
59            workspace.config = serde_json::json!({
60                "name": workspace.name,
61                "description": workspace.description,
62                "folders": [],
63                "requests": []
64            });
65        }
66
67        // Insert into database
68        sqlx::query!(
69            r#"
70            INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
71            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
72            "#,
73            workspace.id,
74            workspace.name,
75            workspace.description,
76            workspace.owner_id,
77            workspace.config,
78            workspace.version,
79            workspace.created_at,
80            workspace.updated_at,
81            workspace.is_archived
82        )
83        .execute(&self.db)
84        .await?;
85
86        // Add owner as admin member
87        let member = WorkspaceMember::new(workspace.id, owner_id, UserRole::Admin);
88        sqlx::query!(
89            r#"
90            INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
91            VALUES (?, ?, ?, ?, ?, ?)
92            "#,
93            member.id,
94            member.workspace_id,
95            member.user_id,
96            member.role,
97            member.joined_at,
98            member.last_activity
99        )
100        .execute(&self.db)
101        .await?;
102
103        // Update cache
104        self.cache.write().insert(workspace.id, workspace.clone());
105
106        Ok(workspace)
107    }
108
109    /// Get a workspace by ID
110    pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
111        // Check cache first
112        if let Some(workspace) = self.cache.read().get(&workspace_id) {
113            return Ok(workspace.clone());
114        }
115
116        // Query database
117        let workspace = sqlx::query_as!(
118            TeamWorkspace,
119            r#"
120            SELECT
121                id as "id: Uuid",
122                name,
123                description,
124                owner_id as "owner_id: Uuid",
125                config,
126                version,
127                created_at as "created_at: chrono::DateTime<chrono::Utc>",
128                updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
129                is_archived as "is_archived: bool"
130            FROM workspaces
131            WHERE id = ?
132            "#,
133            workspace_id
134        )
135        .fetch_optional(&self.db)
136        .await?
137        .ok_or_else(|| CollabError::WorkspaceNotFound(workspace_id.to_string()))?;
138
139        // Update cache
140        self.cache.write().insert(workspace_id, workspace.clone());
141
142        Ok(workspace)
143    }
144
145    /// Update a workspace
146    pub async fn update_workspace(
147        &self,
148        workspace_id: Uuid,
149        user_id: Uuid,
150        name: Option<String>,
151        description: Option<String>,
152        config: Option<serde_json::Value>,
153    ) -> Result<TeamWorkspace> {
154        // Check permissions
155        let member = self.get_member(workspace_id, user_id).await?;
156        PermissionChecker::check(member.role, Permission::WorkspaceUpdate)?;
157
158        let mut workspace = self.get_workspace(workspace_id).await?;
159
160        // Update fields
161        if let Some(name) = name {
162            workspace.name = name;
163        }
164        if let Some(description) = description {
165            workspace.description = Some(description);
166        }
167        if let Some(config) = config {
168            workspace.config = config;
169        }
170        workspace.updated_at = Utc::now();
171        workspace.version += 1;
172
173        // Save to database
174        sqlx::query!(
175            r#"
176            UPDATE workspaces
177            SET name = ?, description = ?, config = ?, version = ?, updated_at = ?
178            WHERE id = ?
179            "#,
180            workspace.name,
181            workspace.description,
182            workspace.config,
183            workspace.version,
184            workspace.updated_at,
185            workspace.id
186        )
187        .execute(&self.db)
188        .await?;
189
190        // Update cache
191        self.cache.write().insert(workspace_id, workspace.clone());
192
193        Ok(workspace)
194    }
195
196    /// Delete (archive) a workspace
197    pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
198        // Check permissions
199        let member = self.get_member(workspace_id, user_id).await?;
200        PermissionChecker::check(member.role, Permission::WorkspaceDelete)?;
201
202        let now = Utc::now();
203        sqlx::query!(
204            r#"
205            UPDATE workspaces
206            SET is_archived = TRUE, updated_at = ?
207            WHERE id = ?
208            "#,
209            now,
210            workspace_id
211        )
212        .execute(&self.db)
213        .await?;
214
215        // Remove from cache
216        self.cache.write().remove(&workspace_id);
217
218        Ok(())
219    }
220
221    /// Add a member to a workspace
222    pub async fn add_member(
223        &self,
224        workspace_id: Uuid,
225        user_id: Uuid,
226        new_member_id: Uuid,
227        role: UserRole,
228    ) -> Result<WorkspaceMember> {
229        // Check permissions
230        let member = self.get_member(workspace_id, user_id).await?;
231        PermissionChecker::check(member.role, Permission::InviteMembers)?;
232
233        // Create new member
234        let new_member = WorkspaceMember::new(workspace_id, new_member_id, role);
235
236        sqlx::query!(
237            r#"
238            INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
239            VALUES (?, ?, ?, ?, ?, ?)
240            "#,
241            new_member.id,
242            new_member.workspace_id,
243            new_member.user_id,
244            new_member.role,
245            new_member.joined_at,
246            new_member.last_activity
247        )
248        .execute(&self.db)
249        .await?;
250
251        Ok(new_member)
252    }
253
254    /// Remove a member from a workspace
255    pub async fn remove_member(
256        &self,
257        workspace_id: Uuid,
258        user_id: Uuid,
259        member_to_remove: Uuid,
260    ) -> Result<()> {
261        // Check permissions
262        let member = self.get_member(workspace_id, user_id).await?;
263        PermissionChecker::check(member.role, Permission::RemoveMembers)?;
264
265        // Don't allow removing the owner
266        let workspace = self.get_workspace(workspace_id).await?;
267        if member_to_remove == workspace.owner_id {
268            return Err(CollabError::InvalidInput("Cannot remove workspace owner".to_string()));
269        }
270
271        sqlx::query!(
272            r#"
273            DELETE FROM workspace_members
274            WHERE workspace_id = ? AND user_id = ?
275            "#,
276            workspace_id,
277            member_to_remove
278        )
279        .execute(&self.db)
280        .await?;
281
282        Ok(())
283    }
284
285    /// Change a member's role
286    pub async fn change_role(
287        &self,
288        workspace_id: Uuid,
289        user_id: Uuid,
290        member_id: Uuid,
291        new_role: UserRole,
292    ) -> Result<WorkspaceMember> {
293        // Check permissions
294        let member = self.get_member(workspace_id, user_id).await?;
295        PermissionChecker::check(member.role, Permission::ChangeRoles)?;
296
297        // Don't allow changing the owner's role
298        let workspace = self.get_workspace(workspace_id).await?;
299        if member_id == workspace.owner_id {
300            return Err(CollabError::InvalidInput(
301                "Cannot change workspace owner's role".to_string(),
302            ));
303        }
304
305        sqlx::query!(
306            r#"
307            UPDATE workspace_members
308            SET role = ?
309            WHERE workspace_id = ? AND user_id = ?
310            "#,
311            new_role,
312            workspace_id,
313            member_id
314        )
315        .execute(&self.db)
316        .await?;
317
318        self.get_member(workspace_id, member_id).await
319    }
320
321    /// Get a workspace member
322    pub async fn get_member(&self, workspace_id: Uuid, user_id: Uuid) -> Result<WorkspaceMember> {
323        sqlx::query_as!(
324            WorkspaceMember,
325            r#"
326            SELECT
327                id as "id: Uuid",
328                workspace_id as "workspace_id: Uuid",
329                user_id as "user_id: Uuid",
330                role as "role: UserRole",
331                joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
332                last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
333            FROM workspace_members
334            WHERE workspace_id = ? AND user_id = ?
335            "#,
336            workspace_id,
337            user_id
338        )
339        .fetch_optional(&self.db)
340        .await?
341        .ok_or_else(|| CollabError::AuthorizationFailed("User is not a member".to_string()))
342    }
343
344    /// List all members of a workspace
345    pub async fn list_members(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMember>> {
346        let members = sqlx::query_as!(
347            WorkspaceMember,
348            r#"
349            SELECT
350                id as "id: Uuid",
351                workspace_id as "workspace_id: Uuid",
352                user_id as "user_id: Uuid",
353                role as "role: UserRole",
354                joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
355                last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
356            FROM workspace_members
357            WHERE workspace_id = ?
358            ORDER BY joined_at
359            "#,
360            workspace_id
361        )
362        .fetch_all(&self.db)
363        .await?;
364
365        Ok(members)
366    }
367
368    /// List all workspaces for a user
369    pub async fn list_user_workspaces(&self, user_id: Uuid) -> Result<Vec<TeamWorkspace>> {
370        let workspaces = sqlx::query_as!(
371            TeamWorkspace,
372            r#"
373            SELECT
374                w.id as "id: Uuid",
375                w.name,
376                w.description,
377                w.owner_id as "owner_id: Uuid",
378                w.config,
379                w.version,
380                w.created_at as "created_at: chrono::DateTime<chrono::Utc>",
381                w.updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
382                w.is_archived as "is_archived: bool"
383            FROM workspaces w
384            INNER JOIN workspace_members m ON w.id = m.workspace_id
385            WHERE m.user_id = ? AND w.is_archived = FALSE
386            ORDER BY w.updated_at DESC
387            "#,
388            user_id
389        )
390        .fetch_all(&self.db)
391        .await?;
392
393        Ok(workspaces)
394    }
395
396    /// Fork a workspace (create an independent copy)
397    ///
398    /// Creates a new workspace that is a copy of the source workspace.
399    /// The forked workspace has its own ID and can be modified independently.
400    pub async fn fork_workspace(
401        &self,
402        source_workspace_id: Uuid,
403        new_name: Option<String>,
404        new_owner_id: Uuid,
405        fork_point_commit_id: Option<Uuid>,
406    ) -> Result<TeamWorkspace> {
407        // Verify user has access to source workspace
408        self.get_member(source_workspace_id, new_owner_id).await?;
409
410        // Get source workspace
411        let source_workspace = self.get_workspace(source_workspace_id).await?;
412
413        // Create new workspace with copied data
414        let mut forked_workspace = TeamWorkspace::new(
415            new_name.unwrap_or_else(|| format!("{} (Fork)", source_workspace.name)),
416            new_owner_id,
417        );
418        forked_workspace.description = source_workspace.description.clone();
419
420        // Deep copy the config (workspace data) to ensure independence
421        // If we have CoreBridge, we can properly clone the core workspace
422        if let Some(core_bridge) = &self.core_bridge {
423            // Get the core workspace from source
424            if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
425                // Generate new IDs for all entities in the forked workspace
426                core_workspace.id = forked_workspace.id.to_string();
427                core_workspace.name = forked_workspace.name.clone();
428                core_workspace.description = forked_workspace.description.clone();
429                core_workspace.created_at = forked_workspace.created_at;
430                core_workspace.updated_at = forked_workspace.updated_at;
431
432                // Regenerate IDs for folders and requests to ensure independence
433                Self::regenerate_entity_ids(&mut core_workspace);
434
435                // Convert back to TeamWorkspace
436                if let Ok(team_ws) = core_bridge.core_to_team(&core_workspace, new_owner_id) {
437                    forked_workspace.config = team_ws.config;
438                } else {
439                    // Fallback to shallow copy
440                    forked_workspace.config = source_workspace.config.clone();
441                }
442            } else {
443                // Fallback to shallow copy
444                forked_workspace.config = source_workspace.config.clone();
445            }
446        } else {
447            // Fallback to shallow copy
448            forked_workspace.config = source_workspace.config.clone();
449        }
450
451        // Insert forked workspace into database
452        sqlx::query!(
453            r#"
454            INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
455            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
456            "#,
457            forked_workspace.id,
458            forked_workspace.name,
459            forked_workspace.description,
460            forked_workspace.owner_id,
461            forked_workspace.config,
462            forked_workspace.version,
463            forked_workspace.created_at,
464            forked_workspace.updated_at,
465            forked_workspace.is_archived
466        )
467        .execute(&self.db)
468        .await?;
469
470        // Add owner as admin member
471        let member = WorkspaceMember::new(forked_workspace.id, new_owner_id, UserRole::Admin);
472        sqlx::query!(
473            r#"
474            INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
475            VALUES (?, ?, ?, ?, ?, ?)
476            "#,
477            member.id,
478            member.workspace_id,
479            member.user_id,
480            member.role,
481            member.joined_at,
482            member.last_activity
483        )
484        .execute(&self.db)
485        .await?;
486
487        // Create fork relationship record
488        let fork = WorkspaceFork::new(
489            source_workspace_id,
490            forked_workspace.id,
491            new_owner_id,
492            fork_point_commit_id,
493        );
494        sqlx::query!(
495            r#"
496            INSERT INTO workspace_forks (id, source_workspace_id, forked_workspace_id, forked_at, forked_by, fork_point_commit_id)
497            VALUES (?, ?, ?, ?, ?, ?)
498            "#,
499            fork.id,
500            fork.source_workspace_id,
501            fork.forked_workspace_id,
502            fork.forked_at,
503            fork.forked_by,
504            fork.fork_point_commit_id
505        )
506        .execute(&self.db)
507        .await?;
508
509        // Update cache
510        self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
511
512        Ok(forked_workspace)
513    }
514
515    /// List all forks of a workspace
516    pub async fn list_forks(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceFork>> {
517        let forks = sqlx::query_as!(
518            WorkspaceFork,
519            r#"
520            SELECT
521                id as "id: Uuid",
522                source_workspace_id as "source_workspace_id: Uuid",
523                forked_workspace_id as "forked_workspace_id: Uuid",
524                forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
525                forked_by as "forked_by: Uuid",
526                fork_point_commit_id as "fork_point_commit_id: Uuid"
527            FROM workspace_forks
528            WHERE source_workspace_id = ?
529            ORDER BY forked_at DESC
530            "#,
531            workspace_id
532        )
533        .fetch_all(&self.db)
534        .await?;
535
536        Ok(forks)
537    }
538
539    /// Get the source workspace for a fork
540    pub async fn get_fork_source(
541        &self,
542        forked_workspace_id: Uuid,
543    ) -> Result<Option<WorkspaceFork>> {
544        let fork = sqlx::query_as!(
545            WorkspaceFork,
546            r#"
547            SELECT
548                id as "id: Uuid",
549                source_workspace_id as "source_workspace_id: Uuid",
550                forked_workspace_id as "forked_workspace_id: Uuid",
551                forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
552                forked_by as "forked_by: Uuid",
553                fork_point_commit_id as "fork_point_commit_id: Uuid"
554            FROM workspace_forks
555            WHERE forked_workspace_id = ?
556            "#,
557            forked_workspace_id
558        )
559        .fetch_optional(&self.db)
560        .await?;
561
562        Ok(fork)
563    }
564
565    /// Regenerate entity IDs in a core workspace to ensure fork independence
566    fn regenerate_entity_ids(core_workspace: &mut mockforge_core::workspace::Workspace) {
567        use mockforge_core::workspace::{Folder, MockRequest};
568        use uuid::Uuid;
569
570        // Regenerate workspace ID
571        core_workspace.id = Uuid::new_v4().to_string();
572
573        // Helper to regenerate folder IDs recursively
574        fn regenerate_folder_ids(folder: &mut Folder) {
575            folder.id = Uuid::new_v4().to_string();
576            for subfolder in &mut folder.folders {
577                regenerate_folder_ids(subfolder);
578            }
579            for request in &mut folder.requests {
580                request.id = Uuid::new_v4().to_string();
581            }
582        }
583
584        // Regenerate IDs for root folders
585        for folder in &mut core_workspace.folders {
586            regenerate_folder_ids(folder);
587        }
588
589        // Regenerate IDs for root requests
590        for request in &mut core_workspace.requests {
591            request.id = Uuid::new_v4().to_string();
592        }
593    }
594}
595
596/// Workspace manager (higher-level API)
597pub struct WorkspaceManager {
598    service: Arc<WorkspaceService>,
599}
600
601impl WorkspaceManager {
602    /// Create a new workspace manager
603    pub fn new(service: Arc<WorkspaceService>) -> Self {
604        Self { service }
605    }
606
607    /// Create and setup a new workspace
608    pub async fn create_workspace(
609        &self,
610        name: String,
611        description: Option<String>,
612        owner_id: Uuid,
613    ) -> Result<TeamWorkspace> {
614        self.service.create_workspace(name, description, owner_id).await
615    }
616
617    /// Get workspace with member check
618    pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
619        // Verify user is a member
620        self.service.get_member(workspace_id, user_id).await?;
621        self.service.get_workspace(workspace_id).await
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    // Note: These tests would require a database setup
630    // For now, they serve as documentation of the API
631}