1use 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
14pub struct WorkspaceService {
16 db: Pool<Sqlite>,
17 cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
18 core_bridge: Option<Arc<CoreBridge>>,
19}
20
21impl WorkspaceService {
22 #[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 #[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 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 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 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 workspace.config = serde_json::json!({
74 "name": workspace.name,
75 "description": workspace.description,
76 "folders": [],
77 "requests": []
78 });
79 }
80
81 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 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 self.cache.write().insert(workspace.id, workspace.clone());
119
120 Ok(workspace)
121 }
122
123 pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
129 if let Some(workspace) = self.cache.read().get(&workspace_id) {
131 return Ok(workspace.clone());
132 }
133
134 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 self.cache.write().insert(workspace_id, workspace.clone());
159
160 Ok(workspace)
161 }
162
163 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 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 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 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 self.cache.write().insert(workspace_id, workspace.clone());
214
215 Ok(workspace)
216 }
217
218 pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
224 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 self.cache.write().remove(&workspace_id);
243
244 Ok(())
245 }
246
247 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 let member = self.get_member(workspace_id, user_id).await?;
261 PermissionChecker::check(member.role, Permission::InviteMembers)?;
262
263 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 pub async fn remove_member(
290 &self,
291 workspace_id: Uuid,
292 user_id: Uuid,
293 member_to_remove: Uuid,
294 ) -> Result<()> {
295 let member = self.get_member(workspace_id, user_id).await?;
297 PermissionChecker::check(member.role, Permission::RemoveMembers)?;
298
299 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 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 let member = self.get_member(workspace_id, user_id).await?;
333 PermissionChecker::check(member.role, Permission::ChangeRoles)?;
334
335 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 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 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 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 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 self.get_member(source_workspace_id, new_owner_id).await?;
463
464 let source_workspace = self.get_workspace(source_workspace_id).await?;
466
467 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 if let Some(core_bridge) = &self.core_bridge {
477 if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
479 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 Self::regenerate_entity_ids(&mut core_workspace);
488
489 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 forked_workspace.config.clone_from(&source_workspace.config);
495 }
496 } else {
497 forked_workspace.config.clone_from(&source_workspace.config);
499 }
500 } else {
501 forked_workspace.config = source_workspace.config.clone();
503 }
504
505 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 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 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 self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
565
566 Ok(forked_workspace)
567 }
568
569 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 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 #[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 core_workspace.id = Uuid::new_v4().to_string();
635
636 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 for folder in &mut core_workspace.folders {
649 regenerate_folder_ids(folder);
650 }
651
652 for request in &mut core_workspace.requests {
654 request.id = Uuid::new_v4().to_string();
655 }
656 }
657}
658
659pub struct WorkspaceManager {
661 service: Arc<WorkspaceService>,
662}
663
664impl WorkspaceManager {
665 #[must_use]
667 pub const fn new(service: Arc<WorkspaceService>) -> Self {
668 Self { service }
669 }
670
671 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 pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
691 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 }