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