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(
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 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 workspace.config = serde_json::json!({
70 "name": workspace.name,
71 "description": workspace.description,
72 "folders": [],
73 "requests": []
74 });
75 }
76
77 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 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 self.cache.write().insert(workspace.id, workspace.clone());
115
116 Ok(workspace)
117 }
118
119 pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
121 if let Some(workspace) = self.cache.read().get(&workspace_id) {
123 return Ok(workspace.clone());
124 }
125
126 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 self.cache.write().insert(workspace_id, workspace.clone());
151
152 Ok(workspace)
153 }
154
155 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 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 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 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 self.cache.write().insert(workspace_id, workspace.clone());
202
203 Ok(workspace)
204 }
205
206 pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
208 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 self.cache.write().remove(&workspace_id);
227
228 Ok(())
229 }
230
231 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 let member = self.get_member(workspace_id, user_id).await?;
241 PermissionChecker::check(member.role, Permission::InviteMembers)?;
242
243 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 pub async fn remove_member(
266 &self,
267 workspace_id: Uuid,
268 user_id: Uuid,
269 member_to_remove: Uuid,
270 ) -> Result<()> {
271 let member = self.get_member(workspace_id, user_id).await?;
273 PermissionChecker::check(member.role, Permission::RemoveMembers)?;
274
275 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 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 let member = self.get_member(workspace_id, user_id).await?;
305 PermissionChecker::check(member.role, Permission::ChangeRoles)?;
306
307 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 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 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 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 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 self.get_member(source_workspace_id, new_owner_id).await?;
419
420 let source_workspace = self.get_workspace(source_workspace_id).await?;
422
423 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 if let Some(core_bridge) = &self.core_bridge {
433 if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
435 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 Self::regenerate_entity_ids(&mut core_workspace);
444
445 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 forked_workspace.config = source_workspace.config.clone();
451 }
452 } else {
453 forked_workspace.config = source_workspace.config.clone();
455 }
456 } else {
457 forked_workspace.config = source_workspace.config.clone();
459 }
460
461 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 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 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 self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
521
522 Ok(forked_workspace)
523 }
524
525 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 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 fn regenerate_entity_ids(core_workspace: &mut mockforge_core::workspace::Workspace) {
577 use mockforge_core::workspace::Folder;
578 use uuid::Uuid;
579
580 core_workspace.id = Uuid::new_v4().to_string();
582
583 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 for folder in &mut core_workspace.folders {
596 regenerate_folder_ids(folder);
597 }
598
599 for request in &mut core_workspace.requests {
601 request.id = Uuid::new_v4().to_string();
602 }
603 }
604}
605
606pub struct WorkspaceManager {
608 service: Arc<WorkspaceService>,
609}
610
611impl WorkspaceManager {
612 #[must_use]
614 pub const fn new(service: Arc<WorkspaceService>) -> Self {
615 Self { service }
616 }
617
618 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 pub async fn get_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<TeamWorkspace> {
630 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 }