Skip to main content

mockforge_collab/
workspace.rs

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