1#![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
18pub struct WorkspaceService {
20 db: Pool<Sqlite>,
21 cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
22 core_bridge: Option<Arc<CoreBridge>>,
23}
24
25impl WorkspaceService {
26 #[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 #[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 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 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 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 workspace.config = serde_json::json!({
78 "name": workspace.name,
79 "description": workspace.description,
80 "folders": [],
81 "requests": []
82 });
83 }
84
85 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 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 self.cache.write().insert(workspace.id, workspace.clone());
123
124 Ok(workspace)
125 }
126
127 pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
133 if let Some(workspace) = self.cache.read().get(&workspace_id) {
135 return Ok(workspace.clone());
136 }
137
138 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 self.cache.write().insert(workspace_id, workspace.clone());
163
164 Ok(workspace)
165 }
166
167 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 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 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 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 self.cache.write().insert(workspace_id, workspace.clone());
218
219 Ok(workspace)
220 }
221
222 pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
228 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 self.cache.write().remove(&workspace_id);
247
248 Ok(())
249 }
250
251 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 let member = self.get_member(workspace_id, user_id).await?;
265 PermissionChecker::check(member.role, Permission::InviteMembers)?;
266
267 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 pub async fn remove_member(
294 &self,
295 workspace_id: Uuid,
296 user_id: Uuid,
297 member_to_remove: Uuid,
298 ) -> Result<()> {
299 let member = self.get_member(workspace_id, user_id).await?;
301 PermissionChecker::check(member.role, Permission::RemoveMembers)?;
302
303 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 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 let member = self.get_member(workspace_id, user_id).await?;
337 PermissionChecker::check(member.role, Permission::ChangeRoles)?;
338
339 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 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 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 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 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 self.get_member(source_workspace_id, new_owner_id).await?;
467
468 let source_workspace = self.get_workspace(source_workspace_id).await?;
470
471 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 if let Some(core_bridge) = &self.core_bridge {
481 if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
483 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 Self::regenerate_entity_ids(&mut core_workspace);
492
493 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 forked_workspace.config.clone_from(&source_workspace.config);
499 }
500 } else {
501 forked_workspace.config.clone_from(&source_workspace.config);
503 }
504 } else {
505 forked_workspace.config = source_workspace.config.clone();
507 }
508
509 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 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 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 self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
569
570 Ok(forked_workspace)
571 }
572
573 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 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 #[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 core_workspace.id = Uuid::new_v4().to_string();
639
640 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 for folder in &mut core_workspace.folders {
653 regenerate_folder_ids(folder);
654 }
655
656 for request in &mut core_workspace.requests {
658 request.id = Uuid::new_v4().to_string();
659 }
660 }
661}
662
663pub struct WorkspaceManager {
665 service: Arc<WorkspaceService>,
666}
667
668impl WorkspaceManager {
669 #[must_use]
671 pub const fn new(service: Arc<WorkspaceService>) -> Self {
672 Self { service }
673 }
674
675 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 pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
695 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 }