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