Skip to main content

mockforge_collab/
workspace.rs

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