1use crate::core_bridge::CoreBridge;
4use crate::error::{CollabError, Result};
5use crate::models::{
6 MergeConflict, MergeStatus, TeamWorkspace, UserRole, WorkspaceFork, WorkspaceMember,
7 WorkspaceMerge,
8};
9use crate::permissions::{Permission, PermissionChecker};
10use chrono::Utc;
11use parking_lot::RwLock;
12use sqlx::{Pool, Sqlite};
13use std::collections::HashMap;
14use std::sync::Arc;
15use uuid::Uuid;
16
17pub struct WorkspaceService {
19 db: Pool<Sqlite>,
20 cache: Arc<RwLock<HashMap<Uuid, TeamWorkspace>>>,
21 core_bridge: Option<Arc<CoreBridge>>,
22}
23
24impl WorkspaceService {
25 pub fn new(db: Pool<Sqlite>) -> Self {
27 Self {
28 db,
29 cache: Arc::new(RwLock::new(HashMap::new())),
30 core_bridge: None,
31 }
32 }
33
34 pub fn with_core_bridge(db: Pool<Sqlite>, core_bridge: Arc<CoreBridge>) -> Self {
36 Self {
37 db,
38 cache: Arc::new(RwLock::new(HashMap::new())),
39 core_bridge: Some(core_bridge),
40 }
41 }
42
43 pub async fn check_database_health(&self) -> bool {
45 match sqlx::query("SELECT 1").execute(&self.db).await {
46 Ok(_) => true,
47 Err(e) => {
48 tracing::error!("Database health check failed: {}", e);
49 false
50 }
51 }
52 }
53
54 pub async fn create_workspace(
56 &self,
57 name: String,
58 description: Option<String>,
59 owner_id: Uuid,
60 ) -> Result<TeamWorkspace> {
61 let mut workspace = TeamWorkspace::new(name.clone(), owner_id);
62 workspace.description = description.clone();
63
64 if let Some(core_bridge) = &self.core_bridge {
66 let core_workspace = core_bridge.create_empty_workspace(name, owner_id)?;
67 workspace.config = core_workspace.config;
68 } else {
69 workspace.config = serde_json::json!({
71 "name": workspace.name,
72 "description": workspace.description,
73 "folders": [],
74 "requests": []
75 });
76 }
77
78 sqlx::query!(
80 r#"
81 INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
82 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
83 "#,
84 workspace.id,
85 workspace.name,
86 workspace.description,
87 workspace.owner_id,
88 workspace.config,
89 workspace.version,
90 workspace.created_at,
91 workspace.updated_at,
92 workspace.is_archived
93 )
94 .execute(&self.db)
95 .await?;
96
97 let member = WorkspaceMember::new(workspace.id, owner_id, UserRole::Admin);
99 sqlx::query!(
100 r#"
101 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
102 VALUES (?, ?, ?, ?, ?, ?)
103 "#,
104 member.id,
105 member.workspace_id,
106 member.user_id,
107 member.role,
108 member.joined_at,
109 member.last_activity
110 )
111 .execute(&self.db)
112 .await?;
113
114 self.cache.write().insert(workspace.id, workspace.clone());
116
117 Ok(workspace)
118 }
119
120 pub async fn get_workspace(&self, workspace_id: Uuid) -> Result<TeamWorkspace> {
122 if let Some(workspace) = self.cache.read().get(&workspace_id) {
124 return Ok(workspace.clone());
125 }
126
127 let workspace = sqlx::query_as!(
129 TeamWorkspace,
130 r#"
131 SELECT
132 id as "id: Uuid",
133 name,
134 description,
135 owner_id as "owner_id: Uuid",
136 config,
137 version,
138 created_at as "created_at: chrono::DateTime<chrono::Utc>",
139 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
140 is_archived as "is_archived: bool"
141 FROM workspaces
142 WHERE id = ?
143 "#,
144 workspace_id
145 )
146 .fetch_optional(&self.db)
147 .await?
148 .ok_or_else(|| CollabError::WorkspaceNotFound(workspace_id.to_string()))?;
149
150 self.cache.write().insert(workspace_id, workspace.clone());
152
153 Ok(workspace)
154 }
155
156 pub async fn update_workspace(
158 &self,
159 workspace_id: Uuid,
160 user_id: Uuid,
161 name: Option<String>,
162 description: Option<String>,
163 config: Option<serde_json::Value>,
164 ) -> Result<TeamWorkspace> {
165 let member = self.get_member(workspace_id, user_id).await?;
167 PermissionChecker::check(member.role, Permission::WorkspaceUpdate)?;
168
169 let mut workspace = self.get_workspace(workspace_id).await?;
170
171 if let Some(name) = name {
173 workspace.name = name;
174 }
175 if let Some(description) = description {
176 workspace.description = Some(description);
177 }
178 if let Some(config) = config {
179 workspace.config = config;
180 }
181 workspace.updated_at = Utc::now();
182 workspace.version += 1;
183
184 sqlx::query!(
186 r#"
187 UPDATE workspaces
188 SET name = ?, description = ?, config = ?, version = ?, updated_at = ?
189 WHERE id = ?
190 "#,
191 workspace.name,
192 workspace.description,
193 workspace.config,
194 workspace.version,
195 workspace.updated_at,
196 workspace.id
197 )
198 .execute(&self.db)
199 .await?;
200
201 self.cache.write().insert(workspace_id, workspace.clone());
203
204 Ok(workspace)
205 }
206
207 pub async fn delete_workspace(&self, workspace_id: Uuid, user_id: Uuid) -> Result<()> {
209 let member = self.get_member(workspace_id, user_id).await?;
211 PermissionChecker::check(member.role, Permission::WorkspaceDelete)?;
212
213 let now = Utc::now();
214 sqlx::query!(
215 r#"
216 UPDATE workspaces
217 SET is_archived = TRUE, updated_at = ?
218 WHERE id = ?
219 "#,
220 now,
221 workspace_id
222 )
223 .execute(&self.db)
224 .await?;
225
226 self.cache.write().remove(&workspace_id);
228
229 Ok(())
230 }
231
232 pub async fn add_member(
234 &self,
235 workspace_id: Uuid,
236 user_id: Uuid,
237 new_member_id: Uuid,
238 role: UserRole,
239 ) -> Result<WorkspaceMember> {
240 let member = self.get_member(workspace_id, user_id).await?;
242 PermissionChecker::check(member.role, Permission::InviteMembers)?;
243
244 let new_member = WorkspaceMember::new(workspace_id, new_member_id, role);
246
247 sqlx::query!(
248 r#"
249 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
250 VALUES (?, ?, ?, ?, ?, ?)
251 "#,
252 new_member.id,
253 new_member.workspace_id,
254 new_member.user_id,
255 new_member.role,
256 new_member.joined_at,
257 new_member.last_activity
258 )
259 .execute(&self.db)
260 .await?;
261
262 Ok(new_member)
263 }
264
265 pub async fn remove_member(
267 &self,
268 workspace_id: Uuid,
269 user_id: Uuid,
270 member_to_remove: Uuid,
271 ) -> Result<()> {
272 let member = self.get_member(workspace_id, user_id).await?;
274 PermissionChecker::check(member.role, Permission::RemoveMembers)?;
275
276 let workspace = self.get_workspace(workspace_id).await?;
278 if member_to_remove == workspace.owner_id {
279 return Err(CollabError::InvalidInput("Cannot remove workspace owner".to_string()));
280 }
281
282 sqlx::query!(
283 r#"
284 DELETE FROM workspace_members
285 WHERE workspace_id = ? AND user_id = ?
286 "#,
287 workspace_id,
288 member_to_remove
289 )
290 .execute(&self.db)
291 .await?;
292
293 Ok(())
294 }
295
296 pub async fn change_role(
298 &self,
299 workspace_id: Uuid,
300 user_id: Uuid,
301 member_id: Uuid,
302 new_role: UserRole,
303 ) -> Result<WorkspaceMember> {
304 let member = self.get_member(workspace_id, user_id).await?;
306 PermissionChecker::check(member.role, Permission::ChangeRoles)?;
307
308 let workspace = self.get_workspace(workspace_id).await?;
310 if member_id == workspace.owner_id {
311 return Err(CollabError::InvalidInput(
312 "Cannot change workspace owner's role".to_string(),
313 ));
314 }
315
316 sqlx::query!(
317 r#"
318 UPDATE workspace_members
319 SET role = ?
320 WHERE workspace_id = ? AND user_id = ?
321 "#,
322 new_role,
323 workspace_id,
324 member_id
325 )
326 .execute(&self.db)
327 .await?;
328
329 self.get_member(workspace_id, member_id).await
330 }
331
332 pub async fn get_member(&self, workspace_id: Uuid, user_id: Uuid) -> Result<WorkspaceMember> {
334 sqlx::query_as!(
335 WorkspaceMember,
336 r#"
337 SELECT
338 id as "id: Uuid",
339 workspace_id as "workspace_id: Uuid",
340 user_id as "user_id: Uuid",
341 role as "role: UserRole",
342 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
343 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
344 FROM workspace_members
345 WHERE workspace_id = ? AND user_id = ?
346 "#,
347 workspace_id,
348 user_id
349 )
350 .fetch_optional(&self.db)
351 .await?
352 .ok_or_else(|| CollabError::AuthorizationFailed("User is not a member".to_string()))
353 }
354
355 pub async fn list_members(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceMember>> {
357 let members = sqlx::query_as!(
358 WorkspaceMember,
359 r#"
360 SELECT
361 id as "id: Uuid",
362 workspace_id as "workspace_id: Uuid",
363 user_id as "user_id: Uuid",
364 role as "role: UserRole",
365 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
366 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
367 FROM workspace_members
368 WHERE workspace_id = ?
369 ORDER BY joined_at
370 "#,
371 workspace_id
372 )
373 .fetch_all(&self.db)
374 .await?;
375
376 Ok(members)
377 }
378
379 pub async fn list_user_workspaces(&self, user_id: Uuid) -> Result<Vec<TeamWorkspace>> {
381 let workspaces = sqlx::query_as!(
382 TeamWorkspace,
383 r#"
384 SELECT
385 w.id as "id: Uuid",
386 w.name,
387 w.description,
388 w.owner_id as "owner_id: Uuid",
389 w.config,
390 w.version,
391 w.created_at as "created_at: chrono::DateTime<chrono::Utc>",
392 w.updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
393 w.is_archived as "is_archived: bool"
394 FROM workspaces w
395 INNER JOIN workspace_members m ON w.id = m.workspace_id
396 WHERE m.user_id = ? AND w.is_archived = FALSE
397 ORDER BY w.updated_at DESC
398 "#,
399 user_id
400 )
401 .fetch_all(&self.db)
402 .await?;
403
404 Ok(workspaces)
405 }
406
407 pub async fn fork_workspace(
412 &self,
413 source_workspace_id: Uuid,
414 new_name: Option<String>,
415 new_owner_id: Uuid,
416 fork_point_commit_id: Option<Uuid>,
417 ) -> Result<TeamWorkspace> {
418 self.get_member(source_workspace_id, new_owner_id).await?;
420
421 let source_workspace = self.get_workspace(source_workspace_id).await?;
423
424 let mut forked_workspace = TeamWorkspace::new(
426 new_name.unwrap_or_else(|| format!("{} (Fork)", source_workspace.name)),
427 new_owner_id,
428 );
429 forked_workspace.description = source_workspace.description.clone();
430
431 if let Some(core_bridge) = &self.core_bridge {
434 if let Ok(mut core_workspace) = core_bridge.team_to_core(&source_workspace) {
436 core_workspace.id = forked_workspace.id.to_string();
438 core_workspace.name = forked_workspace.name.clone();
439 core_workspace.description = forked_workspace.description.clone();
440 core_workspace.created_at = forked_workspace.created_at;
441 core_workspace.updated_at = forked_workspace.updated_at;
442
443 Self::regenerate_entity_ids(&mut core_workspace);
445
446 if let Ok(team_ws) = core_bridge.core_to_team(&core_workspace, new_owner_id) {
448 forked_workspace.config = team_ws.config;
449 } else {
450 forked_workspace.config = source_workspace.config.clone();
452 }
453 } else {
454 forked_workspace.config = source_workspace.config.clone();
456 }
457 } else {
458 forked_workspace.config = source_workspace.config.clone();
460 }
461
462 sqlx::query!(
464 r#"
465 INSERT INTO workspaces (id, name, description, owner_id, config, version, created_at, updated_at, is_archived)
466 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
467 "#,
468 forked_workspace.id,
469 forked_workspace.name,
470 forked_workspace.description,
471 forked_workspace.owner_id,
472 forked_workspace.config,
473 forked_workspace.version,
474 forked_workspace.created_at,
475 forked_workspace.updated_at,
476 forked_workspace.is_archived
477 )
478 .execute(&self.db)
479 .await?;
480
481 let member = WorkspaceMember::new(forked_workspace.id, new_owner_id, UserRole::Admin);
483 sqlx::query!(
484 r#"
485 INSERT INTO workspace_members (id, workspace_id, user_id, role, joined_at, last_activity)
486 VALUES (?, ?, ?, ?, ?, ?)
487 "#,
488 member.id,
489 member.workspace_id,
490 member.user_id,
491 member.role,
492 member.joined_at,
493 member.last_activity
494 )
495 .execute(&self.db)
496 .await?;
497
498 let fork = WorkspaceFork::new(
500 source_workspace_id,
501 forked_workspace.id,
502 new_owner_id,
503 fork_point_commit_id,
504 );
505 sqlx::query!(
506 r#"
507 INSERT INTO workspace_forks (id, source_workspace_id, forked_workspace_id, forked_at, forked_by, fork_point_commit_id)
508 VALUES (?, ?, ?, ?, ?, ?)
509 "#,
510 fork.id,
511 fork.source_workspace_id,
512 fork.forked_workspace_id,
513 fork.forked_at,
514 fork.forked_by,
515 fork.fork_point_commit_id
516 )
517 .execute(&self.db)
518 .await?;
519
520 self.cache.write().insert(forked_workspace.id, forked_workspace.clone());
522
523 Ok(forked_workspace)
524 }
525
526 pub async fn list_forks(&self, workspace_id: Uuid) -> Result<Vec<WorkspaceFork>> {
528 let forks = sqlx::query_as!(
529 WorkspaceFork,
530 r#"
531 SELECT
532 id as "id: Uuid",
533 source_workspace_id as "source_workspace_id: Uuid",
534 forked_workspace_id as "forked_workspace_id: Uuid",
535 forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
536 forked_by as "forked_by: Uuid",
537 fork_point_commit_id as "fork_point_commit_id: Uuid"
538 FROM workspace_forks
539 WHERE source_workspace_id = ?
540 ORDER BY forked_at DESC
541 "#,
542 workspace_id
543 )
544 .fetch_all(&self.db)
545 .await?;
546
547 Ok(forks)
548 }
549
550 pub async fn get_fork_source(
552 &self,
553 forked_workspace_id: Uuid,
554 ) -> Result<Option<WorkspaceFork>> {
555 let fork = sqlx::query_as!(
556 WorkspaceFork,
557 r#"
558 SELECT
559 id as "id: Uuid",
560 source_workspace_id as "source_workspace_id: Uuid",
561 forked_workspace_id as "forked_workspace_id: Uuid",
562 forked_at as "forked_at: chrono::DateTime<chrono::Utc>",
563 forked_by as "forked_by: Uuid",
564 fork_point_commit_id as "fork_point_commit_id: Uuid"
565 FROM workspace_forks
566 WHERE forked_workspace_id = ?
567 "#,
568 forked_workspace_id
569 )
570 .fetch_optional(&self.db)
571 .await?;
572
573 Ok(fork)
574 }
575
576 fn regenerate_entity_ids(core_workspace: &mut mockforge_core::workspace::Workspace) {
578 use mockforge_core::workspace::{Folder, MockRequest};
579 use uuid::Uuid;
580
581 core_workspace.id = Uuid::new_v4().to_string();
583
584 fn regenerate_folder_ids(folder: &mut Folder) {
586 folder.id = Uuid::new_v4().to_string();
587 for subfolder in &mut folder.folders {
588 regenerate_folder_ids(subfolder);
589 }
590 for request in &mut folder.requests {
591 request.id = Uuid::new_v4().to_string();
592 }
593 }
594
595 for folder in &mut core_workspace.folders {
597 regenerate_folder_ids(folder);
598 }
599
600 for request in &mut core_workspace.requests {
602 request.id = Uuid::new_v4().to_string();
603 }
604 }
605}
606
607pub struct WorkspaceManager {
609 service: Arc<WorkspaceService>,
610}
611
612impl WorkspaceManager {
613 pub 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 }